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 and Map / garbage elements on a list decode are rejected with a clear expected: ... before your factory ever sees them.
  • Exception wrapping with full context. Anything thrown inside the factory becomes a DecodeException with cause, causeTrace, and an optional context label (endpoint name, cache key).
  • Complete list traversal. decodeList never stops on the first broken element. It collects every failure with its index and throws one aggregated DecodeListException — 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.verbose is 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.
  • T is inferred from the tear-off; never pass explicit type arguments (guard.decode<User>(...) is redundant).
  • The json argument must be already-parsed JSON (e.g. dio's response.data). For a raw String body, call jsonDecode(s) first — passing the string throws DecodeException(expected: Map).
  • decode never returns null: a null or absent body throws. There is no nullable variant; do not write ?? fallback after it.
  • decode throws DecodeException; decodeList throws DecodeListException (all broken elements aggregated). They share no supertype besides Exception — catch both where both can occur.
  • Read aggregated failures as records: for (final f in e.errors) { use(f.index); use(f.error); }errors is not a List<DecodeException>.
  • The list returned by decodeList is 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 = true only 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.

Libraries

json_shield
A minimal, dependency-free JSON decoding guard for immutable fromJson-style models (quicktype, json_serializable, hand-written).