skip to content

JSON & Codecs

jsonEncode and jsonDecode map between JSON text and dynamic maps and lists, and the utf8, base64 and latin1 codecs turn strings into bytes and back. Interviewers ask why decoded JSON needs casts.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Dart, what does jsonDecode return, and why does decoded JSON need casts or patterns before you can use it as typed data?

level: juniorimportance: must knowfreq 66%

answer

  1. the return type is dynamic
  2. objects become Map<String, dynamic>
  3. arrays become List<dynamic>
  4. as List<String> fails, cast<String>() works
  5. whole numbers decode as int

basics

~10 s

jsonDecode returns dynamic: objects become Map<String, dynamic>, arrays List<dynamic>, numbers int or double. Nothing is checked against your types, so you cast, convert lists with cast() or List.from, or validate with a pattern.

solid answer

~40 s

`jsonDecode(source)` returns `dynamic`, and the value inside is built only from `Map<String, dynamic>`, `List<dynamic>`, `String`, `num`, `bool` and `null`. Dart cannot know your schema, so the compiler lets `dynamic` flow anywhere and failures appear at runtime. Three traps: `as List<String>` throws, because the runtime object is a `List<dynamic>` whatever it contains, so use `.cast<String>()` or `List<String>.from(...)`; a whole number such as `1` decodes as `int`, so `as double` fails and `(x as num).toDouble()` is safer; and a missing key yields `null`. I validate the shape with a Dart 3 pattern such as `if (data case {'theme': String theme})`, or in a `fromJson` factory, and enable `strict-casts` so implicit `dynamic` downcasts show up in analysis. Invalid JSON text throws a `FormatException`.

code

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

void main() {
  const text =
      '{"theme": "dark", "fontScale": 1, "pinned": ["java", "dart"], "version": 3}';
  final data = jsonDecode(text); // dynamic

  final map = data as Map<String, dynamic>; // fine: the decoder builds this type
  // map['pinned'] as List<String>; // TypeError: it is a List<dynamic>
  final pinned = (map['pinned'] as List).cast<String>();
  // map['fontScale'] as double; // TypeError: 1 decodes as int
  final fontScale = (map['fontScale'] as num).toDouble();

  if (data case {'theme': String theme, 'version': int version}) {
    print('$theme v$version $pinned $fontScale'); // dark v3 [java, dart] 1.0
  }
}

go deeper

for a junior

Know that jsonDecode returns dynamic with maps and lists inside, and that you must cast values before using them as typed data.

for a middle

Explain why as List<String> fails while cast<String>() or List.from works, the int-versus-double trap, and FormatException for bad text.

for a senior

Validate at the boundary with patterns or fromJson factories, turn on strict-casts, and keep dynamic from leaking into widgets or business logic.

for a principal

Decide where schema checking lives, generated models, hand-written parsers or server contracts, and how the app degrades when a payload does not match.

## What comes out of `jsonDecode` `jsonDecode` in `dart:convert` is shorthand for `json.decode`. Its declared return type is **`dynamic`**, and the `JsonDecoder` documentation lists what the value can contain: | JSON | Dart value | |---|---| | object `{...}` | `Map<String, dynamic>` | | array `[...]` | `List<dynamic>` | | string | `String` | | number | `int` for whole numbers, otherwise `double` | | `true` / `false` | `bool` | | `null` | `null` | The containers are created as `Map<String, dynamic>` and `List<dynamic>` regardless of what they hold. That runtime type is what every cast is checked against. ## Why casts are needed Dart's type system cannot see inside a JSON string, so `dynamic` switches off static checking. Code like `final List<String> tags = jsonDecode(text)['tags'];` compiles, because `dynamic` is implicitly downcast, and then fails when it runs. Three runtime failures account for most bugs: 1. **Generic list casts.** `decoded['pinned'] as List<String>` throws a `TypeError`, since the object's runtime type is `List<dynamic>` even if every element is a string. Use `(decoded['pinned'] as List).cast<String>()`, which returns a view that checks each element when read, or `List<String>.from(...)`, which copies and checks eagerly. 2. **Numbers.** JSON has one number type. `1` decodes as `int`, `1.0` and `1.2` as `double`, so `value as double` breaks as soon as a writer emits `1`. Read numbers as `num` and convert with `toDouble()` or `toInt()`. 3. **Missing or null keys.** `map['fontScale']` is `null` when the key is absent, and a non-nullable cast throws. Decide on defaults explicitly. Casting the top-level object with `as Map<String, dynamic>` does work, because that is exactly the map type the decoder creates. ## Cast idioms that work | Goal | Idiom | |---|---| | a required string | `map['theme'] as String` | | an optional string | `map['note'] as String?` | | any number as a double | `(map['fontScale'] as num).toDouble()` | | a list of strings | `(map['pinned'] as List).cast<String>()` or `List<String>.from(...)` | | a nested object | `map['display'] as Map<String, dynamic>` | | a map of ints | `Map<String, int>.from(map['limits'] as Map)` | Each idiom either succeeds with a correctly typed value or throws at the line that reads the field, which makes a bad payload easy to locate. ## Validating with patterns Dart 3 patterns make shape checks declarative. An `if-case` statement checks types, keys and structure at once and binds typed variables: - `if (data case {'theme': String theme, 'version': int version}) { ... }` confirms `data` is a map, both keys exist, and the values have the stated types. - A `switch` over the decoded value can route different payload shapes to different branches. - Anything that fails the pattern falls through to your error handling instead of throwing deep inside a widget. This is the dart.dev recommendation for data from external sources: validate the structure first rather than trusting it. ## Making the analyzer help - `analyzer: language: strict-casts: true` in `analysis_options.yaml` reports every implicit downcast from `dynamic`, such as passing `jsonDecode(text)` straight to a `List<String>` parameter. - Declaring decoded values as `Object?` instead of `dynamic` forces an explicit check or cast before use. - Model classes with `fromJson` factories, hand-written or generated, keep the casts in one place. ## Errors to expect - **Invalid JSON text**, such as a trailing comma or single quotes, makes `jsonDecode` throw a `FormatException`. - **Wrong shapes** surface as `TypeError`s at the first cast that disagrees, which may be far from the decode call. - Catch and report both at the boundary where data enters the app, so the rest of the code works with typed values. ## Summary for an interview Say that `jsonDecode` returns `dynamic` built from `Map<String, dynamic>`, `List<dynamic>` and primitives, explain why `as List<String>` fails while `cast` works, mention the `int` versus `double` trap, and show a pattern or `fromJson` validation step. That covers what interviewers mean when they ask why decoded JSON needs casts.

  • What is the difference between list.cast<String>() and List<String>.from(list)?
    `cast<String>()` returns a lazy view over the original `List<dynamic>` that checks each element's type when it is read, so a bad element fails later, at access. `List<String>.from(list)` copies the elements into a new list and checks them all immediately, which fails early and gives an independent list.
  • What happens when jsonDecode is given text with a trailing comma?
    It throws a `FormatException`, because trailing commas are not valid JSON. Catch it where the text enters the app, such as after reading a settings file, and fall back to defaults or report the corrupt file.
  • How does strict-casts in analysis_options.yaml help with JSON code?
    With `strict-casts: true` the analyzer stops allowing implicit downcasts from `dynamic`. Passing `jsonDecode(text)` straight to a `List<String>` parameter becomes an error, so every conversion from decoded JSON has to be written explicitly, which is where the runtime failures would otherwise hide.

saying these in an interview costs you the question

  • jsonDecode returns a typed object matching the JSON's structure
  • A JSON array of strings decodes to a List<String>
  • Every JSON number decodes to a double
  • Casting with as List<String> is the safe way to type a decoded list
  • jsonDecode returns null for invalid JSON text
open as a page

In Dart, how does jsonEncode handle a custom settings object, and how do toJson(), toEncodable and JsonEncoder.withIndent produce pretty-printed JSON?

level: middleimportance: must knowfreq 52%

basics

~10 s

jsonEncode writes numbers, strings, booleans, null, lists and string-keyed maps directly; for anything else it calls toEncodable, which by default calls the object's toJson(). For pretty output use JsonEncoder.withIndent(' ').convert(settings).

open as a page

In Dart, how do the utf8, base64 and latin1 codecs differ, and what happens when utf8.decode receives bytes that are not valid UTF-8?

level: middleimportance: should knowfreq 38%

basics

~10 s

utf8 and latin1 convert between String and bytes; base64 converts bytes to ASCII text and back. utf8.decode throws a FormatException on malformed bytes unless allowMalformed is true, which substitutes U+FFFD.

open as a page

In Dart, how do Codec.fuse, Converter chaining and LineSplitter let you process a large JSON Lines byte stream without building one giant string?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Converters such as utf8.decoder implement StreamTransformer, so a byte stream can be piped through utf8.decoder and LineSplitter and each line decoded separately. fuse joins codecs or converters, and JSON with UTF-8 fused skips the intermediate string.

open as a page

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%

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.

open as a page