json_shield 0.1.1
json_shield: ^0.1.1 copied to clipboard
Dependency-free JSON decoding guard for fromJson-style models. Shape checks, typed exceptions with cause and trace, full-traversal list decoding with aggregated failures, redacted-by-default payloads.
json_shield #
A minimal, dependency-free JSON decoding guard for immutable fromJson-style
models (quicktype, json_serializable output, hand-written factories).
Turns anonymous runtime failures like
type 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>'
into a single typed exception carrying the source label, the expectation, the raw payload (opt-in), the original cause, and its stack trace.
Why #
fromJson factories are brittle by design: an unexpected null body, a
pagination envelope instead of a plain array, or a backend silently changing
age: int to age: string all surface as bare TypeErrors with no
attribution — you cannot tell from a crash report which endpoint broke.
json_shield wraps the decode call sites with:
- Shape guards before the factory runs.
null/List/ primitive on a single-object decode andMap/ garbage elements on a list decode are rejected with a clearexpected: ...before your factory ever sees them. - Exception wrapping with full context. Anything thrown inside the
factory becomes a
DecodeExceptionwithcause,causeTrace, and an optionalcontextlabel (endpoint name, cache key). - Complete list traversal.
decodeListnever stops on the first broken element. It collects every failure with its index and throws one aggregatedDecodeListException— one run gives you the full map of a broken contract instead of one error per deploy. - Redacted-by-default payloads, loud development. Raw JSON is attached
to exceptions — and every failure is printed to the console — only when
guard.verboseis enabled, so production crash reports never leak payload data by accident, while a dev run surfaces every broken element the moment it happens.
Zero dependencies. Pure Dart. Your generated models are used as-is — nothing to annotate, extend, or regenerate.
Usage #
import 'package:json_shield/json_shield.dart';
// Any fromJson-style model, e.g. quicktype output — used unmodified:
// class User {
// factory User.fromJson(Map<String, dynamic> json) => ...;
// }
// Single object. T is inferred from the tear-off.
final user = guard.decode(User.fromJson, response.data);
// With a source label for log/crash-report attribution.
final me = guard.decode(User.fromJson, response.data, context: 'users_me_get');
// Top-level array. Full traversal, aggregated failure.
final orders = guard.decodeList(Order.fromJson, response.data,
context: 'orders_list');
Handling failures:
try {
final orders = guard.decodeList(Order.fromJson, response.data,
context: 'orders_list');
} on DecodeListException catch (e) {
// e.total -> 100
// e.errors.length -> 3
// e.errors[0] -> (index: 17, error: DecodeException(...))
// toString():
// DecodeListException: [orders_list] 3/100 failed. #17: ...; #41: ...; #98: ...
report(e);
} on DecodeException catch (e) {
// Broken array shape (pagination envelope, null body) or a
// single-object decode failure.
// e.context, e.expected, e.cause, e.causeTrace, e.rawData (verbose only)
report(e);
}
Diagnostic payloads during development:
void main() {
guard.verbose = true; // raw payloads on exceptions + console log of every failure; default: false
runApp(const App());
}
Failure matrix #
| Input | decode |
decodeList |
|---|---|---|
valid Map |
T |
DecodeException(expected: List) |
valid List<Map> |
DecodeException(expected: Map) |
List<T> (unmodifiable) |
null (empty body) |
DecodeException(expected: Map) |
DecodeException(expected: List) |
primitive ("ok", 42) |
DecodeException(expected: Map) |
DecodeException(expected: List) |
Map<dynamic, dynamic>, String keys |
normalized, T |
— |
Map with a non-String key |
DecodeException with cause |
— |
factory throws (TypeError, missing field) |
DecodeException with cause + trace |
collected per element |
| broken element(s) inside the array | — | DecodeListException with every (index, error) |
API #
| Member | Description |
|---|---|
guard.decode<T>(fromJson, json, {context}) |
JSON object → T |
guard.decodeList<T>(fromJson, json, {context}) |
JSON array → List<T>, full traversal, aggregated throw |
guard.verbose |
attach raw payloads + print failures to console (default false) |
DecodeException |
single failure: expected, rawData, context, cause, causeTrace |
DecodeListException |
aggregate: total, errors: List<({int index, DecodeException error})> |
FromJson<T> |
T Function(Map<String, dynamic>) |
Non-goals #
HTTP clients, retries, code generation, schema validation, and state-management integration are intentionally out of scope.
For AI assistants #
Copy these shapes verbatim:
import 'package:json_shield/json_shield.dart';
final user = guard.decode(User.fromJson, response.data); // single object
final users = guard.decodeList(Order.fromJson, response.data); // top-level array
- Argument order is
(fromJson, json): factory tear-off first, data second. Tis inferred from the tear-off; never pass explicit type arguments (guard.decode<User>(...)is redundant).- The
jsonargument must be already-parsed JSON (e.g. dio'sresponse.data). For a rawStringbody, calljsonDecode(s)first — passing the string throwsDecodeException(expected: Map). decodenever returnsnull: anullor absent body throws. There is no nullable variant; do not write?? fallbackafter it.decodethrowsDecodeException;decodeListthrowsDecodeListException(all broken elements aggregated). They share no supertype besidesException— catch both where both can occur.- Read aggregated failures as records:
for (final f in e.errors) { use(f.index); use(f.error); }—errorsis not aList<DecodeException>. - The list returned by
decodeListis unmodifiable; call.toList()on it before adding or removing elements. - Always pass
context: '<endpoint_name>'in application code; it is the only source attribution in production crash reports. guard.verbose = trueonly in a debug entry point — it attaches raw payloads to exceptions and prints every failure to the console; enabled in release it leaks response data into crash reports.- Do not swallow these exceptions with a bare
try/catch; they are the package's error channel. - Out of scope — do not suggest this package for: HTTP calls, retries, schema validation, code generation.