skip to content

In Dart, what does the reviver argument of jsonDecode do, and when is it a better fit than converting values after decoding?

level: middleimportance: nice to knowfreq 18%

answer

  1. called once per property and element
  2. key: String, int index, or null
  3. innermost values first, root last
  4. return value replaces the parsed one
  5. sees no path, only key and value

basics

~20 s

A reviver is called for every parsed property, list element and finally the root, with a String key, int index or null; its return value replaces the parsed value, for example turning ISO strings into DateTime.

solid answer

~50 s

`jsonDecode(source, reviver: fn)` calls `fn(key, value)` once for each property of every JSON object and each element of every array, and finally for the whole result. The key is the property name as a `String`, the list index as an `int`, or `null` for the root; whatever `fn` returns is stored instead of the parsed value. Values are revived from the inside out, so a reviver on a map already sees its revived children. Its typical use is a cross-cutting conversion by value shape, such as turning every UTC ISO timestamp string into a `DateTime`. It is a poor fit for model-specific rules, because it gets no path: it cannot tell `lastSync` in settings from a user's note that happens to look like a date. For typed models, decode plainly and convert in `fromJson`. `JsonCodec.withReviver` and `JsonDecoder(reviver)` bake one in.

code

dart · 15 lines
dart
import 'dart:convert';

final _utcIso = RegExp(r'^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z$');

Object? reviveDates(Object? key, Object? value) =>
    value is String && _utcIso.hasMatch(value) ? DateTime.parse(value) : value;

void main() {
  final data = jsonDecode(
    '{"lastSync": "2026-09-29T08:00:00.000Z", "history": ["2026-09-01T10:00:00Z"]}',
    reviver: reviveDates,
  ) as Map<String, dynamic>;
  print(data['lastSync'] is DateTime); // true
  print((data['history'] as List).first is DateTime); // true
}

go deeper

for a junior

Know that jsonDecode can take a reviver function that sees each key and value and can replace the value.

for a middle

Explain the three key types, the inside-out order, and the timestamp-conversion use case.

for a senior

Judge when a reviver is appropriate: value-shape rules only, not model mapping, and remember encoding needs the inverse conversion.

for a principal

Decide whether cross-cutting conversions belong in a shared codec or in generated models, keeping one source of truth for wire formats.

## The hook `jsonDecode` has one optional named parameter, **`reviver`**, typed `Object? Function(Object? key, Object? value)`. The documentation defines when it runs: - once for **each property of each JSON object**, with the property name as a `String` key; - once for **each element of each JSON array**, with the element's **`int` index** as the key; - once more for the **final result**, with a **`null`** key. Whatever the reviver returns is used in place of the parsed value. The default reviver is the identity function, which is what you get when you omit it. ## Order of calls Parsing builds values from the inside out: 1. Scalars inside the innermost containers are revived first. 2. A container is completed from its already-revived children. 3. The completed container is then passed to the reviver as the value of its own key. 4. Finally the root value is passed with `key == null`. So a reviver that replaces a map with a model object sees a map whose values have already been revived. ## A good use: converting by value shape The classic example is dates. JSON has no date type, so timestamps travel as strings. A reviver that checks whether a string looks like a UTC ISO 8601 timestamp, and returns `DateTime.parse(value)` if so, converts every timestamp in the document during the single parse, at any depth, without walking the structure afterwards. | Conversion | Reviver fits? | Why | |---|---|---| | all ISO timestamp strings to `DateTime` | yes | rule depends only on the value | | strip a known noise key from every object | yes | rule depends only on the key | | settings map to `AppSettings` | poorly | needs to know which object it is | | `theme` string to an enum | poorly | same key name may mean different things elsewhere | ## A walk-through For the text `{"a": [1, 2]}` with a reviver that just logs its arguments, the calls are: 1. `reviver(0, 1)`: the first array element, key is its index. 2. `reviver(1, 2)`: the second element. 3. `reviver('a', [1, 2])`: the finished list, as the value of property `a`. 4. `reviver(null, {'a': [1, 2]})`: the finished root map. If step 1 had returned `'one'`, step 3 would receive `['one', 2]`, because the list is built from revived elements. Returning a different type is allowed; the containers are still `List<dynamic>` and `Map<String, dynamic>`. ## The limits - **No path.** The reviver sees a key and a value, never where it sits. A `note` field containing `2026-09-29T08:00:00Z` would be converted too. - **Types stay loose.** The result is still `dynamic` containing `Map<String, dynamic>` and `List<dynamic>`; revived values just change what is inside. - **Encoding is not symmetric.** A revived `DateTime` must be converted back on encode with `toJson()` or `toEncodable`, or encoding fails. - **Cost.** The function runs for every value in the document, which adds up for large payloads. ## Alternatives - **Decode, then convert in `fromJson`.** A model factory knows exactly which fields are dates or enums. This is the usual choice for typed models, whether written by hand or generated. - **Patterns** such as `if (data case {'lastSync': String iso})` validate and extract in one step, then `DateTime.parse(iso)`. - **A shared codec**: `JsonCodec.withReviver(fn)` or `const JsonCodec(reviver: fn)` bakes one reviver into a codec object, useful when one app-wide rule applies to every document. ## Summary for an interview Describe the callback, its three key types and its inside-out order, give the timestamp example, then state the limit: no path, so model-specific conversion belongs in `fromJson`. That balance, knowing the hook exists and knowing when not to use it, is what the question tests.

  • What key does the reviver receive for the third element of a JSON array?
    The integer `2`, the element's index. Object properties get their `String` names, array elements get `int` indexes, and the final call for the whole document gets `null`.
  • How do you apply the same reviver to every decode in an app?
    Create a codec with it, `const JsonCodec(reviver: reviveDates)` or `JsonCodec.withReviver(reviveDates)`, and call its `decode` everywhere, or use `JsonDecoder(reviveDates)` where a converter is needed. That keeps the rule in one place instead of repeating the argument.

saying these in an interview costs you the question

  • The reviver is called only for top-level keys
  • The reviver receives the full path of each value
  • A reviver makes jsonDecode return a typed model class
  • Revived DateTime values encode back to strings automatically
  • Parent maps are revived before their children