checked_json 0.2.1
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. AFormatExceptionwhen 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)andobjects(k)for an object nested in another.JsonObject.of(map)for an object you build yourself, for atoJson, 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 JsonObjectandis JsonObjectdo not check. An extension type is erased at runtime, sox as JsonObjectisx as Map<String, Object?>and fails with aTypeErrorwhere a door would have thrown aFormatException. UseJsonObject.parseinstead, and the same goes for aJsonObjectpattern.- A
JsonObjectis not aMap.obj['k']does not compile, and neither doesobj['k'] = v, so a field is read through a reader orraw, and nothing outside the library writes to one. (Up to 0.1.x it implementedMap<String, Object?>.) It does implementObject, so it is never null and an extension onT extends Objectapplies to it. parsematches the map's runtime type.jsonDecodealways makes a string-keyed map, so its output is fine. AMap<dynamic, dynamic>, as YAML and Flutter platform channels deliver one, gives null even when every key is a string: copy it withMap<String, Object?>.fromfirst.- Whole numbers are exact only below 2^53 in magnitude.
integerreads3or3.0(as some writers spell one, and as a web build reads every whole number) but never3.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, because9007199254740993.0decodes 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.