Skip to main content

checked_json 0.2.1 copy "checked_json: ^0.2.1" to clipboard
checked_json: ^0.2.1 copied to clipboard

A JSON object type you only get by checking a value, with typed readers that name the field that is absent or the wrong shape.

checked_json #

A JSON object you only get by checking a value. Holding a JsonObject means the value was looked at and found to be a JSON object, so nothing downstream checks it again. Its fields are read through typed readers that say which field was wrong and what it should have been.

import 'package:checked_json/checked_json.dart';

final user = JsonObject.decode('{"name": "Ada", "tags": ["maths"]}');

user.string('name');         // 'Ada'
user.strings('tags');        // ['maths']
user.integerOrNull('born');  // null: absent
user.integer('name');        // throws JsonFieldException: `name` is not an integer

The doors in #

The map inside a JsonObject is private, so code outside the library cannot wrap an unchecked value in one. A value becomes one through a door that checks it:

  • JsonObject.decode(text) for wire text. A FormatException when the text is not JSON, or is JSON of another kind.
  • JsonObject.parse(value) for a value that is already decoded. Null when it is not a JSON object.
  • object(k) and objects(k) for an object nested in another.
  • JsonObject.of(map) for an object you build yourself, for a toJson, from a map whose static type is already string-keyed.

The readers #

string, integer, number, boolean, object, list, strings and objects. A plain reader requires its field and throws a JsonFieldException naming the field and the shape it wanted. Its OrNull twin takes an absent or null field as null, and still refuses one of the wrong type. A default for an absent field is the caller's ??, so the default sits where it is used. has(k) tells a field present with a null value apart from an absent one, and raw(k) gives a field as it was decoded.

Building one object from others #

Each of these gives a new JsonObject, so no map has to come out to be spread:

final streaming = body.withFields({'stream': true});   // fields laid over
final request = JsonObject.merge([common, if (effort != null) effort]);
                                                        // later parts win
final plain = streaming.without({'stream'});            // fields dropped
final kept = plain.frozen();                            // read-only copy
final schema = tool.deepFrozen();                       // read-only all the way down

For an object used as a dictionary, whose keys are data rather than names the code knows, keys gives the names and a reader reads each value:

final prices = {for (final m in table.keys) m: table.number(m)};

frozen() copies only the object itself, and nested objects and lists stay shared. deepFrozen() copies and protects every one of them, keeps field and list order, and refuses (with the path) any value jsonEncode couldn't write, including a key that is not a string.

jsonEncode writes a JsonObject as it is, so encoding needs no map. The one way out to a Map is asUncheckedMap(), a read-only view for a parameter declared as a Map, as some other package's API is. A read from it is unchecked, which is what the name says.

JsonFieldException extends FormatException: a bad field is a condition you're meant to catch, not a programming bug. Code that already catches FormatException still catches it.

What it guarantees, and what it doesn't yet #

  • as JsonObject and is JsonObject do not check. An extension type is erased at runtime, so x as JsonObject is x as Map<String, Object?> and fails with a TypeError where a door would have thrown a FormatException. Use JsonObject.parse instead, and the same goes for a JsonObject pattern.
  • A JsonObject is not a Map. obj['k'] does not compile, and neither does obj['k'] = v, so a field is read through a reader or raw, and nothing outside the library writes to one. (Up to 0.1.x it implemented Map<String, Object?>.) It does implement Object, so it is never null and an extension on T extends Object applies to it.
  • parse matches the map's runtime type. jsonDecode always makes a string-keyed map, so its output is fine. A Map<dynamic, dynamic>, as YAML and Flutter platform channels deliver one, gives null even when every key is a string: copy it with Map<String, Object?>.from first.
  • Whole numbers are exact only below 2^53 in magnitude. integer reads 3 or 3.0 (as some writers spell one, and as a web build reads every whole number) but never 3.5. A double is taken only when its size is strictly below 2^53, where it stands for one whole number; 2^53 itself is refused, because 9007199254740993.0 decodes to it too.
  • Nested values are not deep-checked up front. A door checks that the value is an object; each field is checked when its reader reads it.

Licence #

Apache-2.0. Copyright 2026 Enspyr Pty Ltd.

0
likes
160
points
0
downloads

Documentation

API reference

Publisher

verified publisherenspyr.co

Weekly Downloads

A JSON object type you only get by checking a value, with typed readers that name the field that is absent or the wrong shape.

Repository (GitHub)
View/report issues

License

Apache-2.0 (license)

More

Packages that depend on checked_json