skip to content

In a Dart Page<T>.fromJson, why does casting json['items'] to List<Map<String, dynamic>> throw even though every element is a map, and how do you fix it?

level: middleimportance: should knowfreq 45%

answer

  1. decoder builds List<dynamic>
  2. as checks the object's own type
  3. contents do not change the type argument
  4. cast view versus List.from copy
  5. map each element through fromJsonT

basics

~10 s

jsonDecode creates a List<dynamic>, and reified generics mean it stays List<dynamic> whatever it contains, so as List<Map<String, dynamic>> fails. Treat it as List<dynamic> and convert each element, or use cast, List.from or whereType.

solid answer

~40 s

`jsonDecode` has no idea what your model expects, so it builds lists as `List<dynamic>` and objects as `Map<String, dynamic>`. Because Dart generics are reified, `as` checks the list object's **actual** type argument, `dynamic`, and `List<dynamic>` is not a subtype of `List<Map<String, dynamic>>`, so the cast throws a `TypeError` at run time even though each element is a map. Fix it by treating the value as `List<dynamic>` and converting elements: `[for (final e in raw) fromJsonT(e as Map<String, dynamic>)]` is the clearest. Alternatives: `raw.cast<Map<String, dynamic>>()` returns a lazy view that checks each element when it is read; `List<Map<String, dynamic>>.from(raw)` copies and checks eagerly; `raw.whereType<Map<String, dynamic>>()` silently drops non-matching elements. In a generic `Page<T>`, take a `T Function(Map<String, dynamic>)` parameter, because `T.fromJson` cannot be called.

code

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

class Page<T> {
  Page({required this.items, this.nextCursor});

  final List<T> items;
  final String? nextCursor;

  factory Page.fromJson(
    Map<String, dynamic> json,
    T Function(Map<String, dynamic> item) fromJsonT,
  ) {
    final rawItems = json['items'] as List<dynamic>;
    return Page(
      items: [for (final item in rawItems) fromJsonT(item as Map<String, dynamic>)],
      nextCursor: json['next_cursor'] as String?,
    );
  }
}

class Order {
  Order(this.id);
  final String id;

  factory Order.fromJson(Map<String, dynamic> json) => Order(json['id'] as String);
}

void main() {
  final json = jsonDecode('{"items": [{"id": "a1"}, {"id": "a2"}], "next_cursor": null}')
      as Map<String, dynamic>;

  print(json['items'] is List<Map<String, dynamic>>); // false: it is a List<dynamic>

  final page = Page.fromJson(json, Order.fromJson);
  print(page.items is List<Order>); // true
  print(page.items.map((o) => o.id).toList()); // [a1, a2]
}

go deeper

for a junior

Recall that decoded JSON lists are List<dynamic> and cannot be cast straight to a list of a specific type.

for a middle

Explain how reification makes the runtime type argument fixed at creation, and compare cast, List.from, whereType and element-wise conversion.

for a senior

Build parse boundaries that fail early with context, pass item parsers into generic wrappers, and test with jsonDecode output rather than Dart literals.

for a principal

Decide between hand-written parsing and generated serialization for generic envelopes across many endpoints, weighing error reporting and maintenance.

## The failure A generic paginated wrapper parses an API response: ```dart factory Page.fromJson(Map<String, dynamic> json, T Function(Map<String, dynamic>) fromJsonT) { final items = json['items'] as List<Map<String, dynamic>>; // throws ... } ``` Every element of `items` is a JSON object, yet the cast fails with a `TypeError` along the lines of *type 'List<dynamic>' is not a subtype of type 'List<Map<String, dynamic>>' in type cast*. The same code often "works" in a unit test that builds the input as a Dart literal, which makes the bug confusing. ## Why it happens 1. **The decoder cannot know your types.** `jsonDecode` returns `dynamic`; JSON arrays become lists created as `List<dynamic>`, and JSON objects become `Map<String, dynamic>`. 2. **Generics are reified.** Each list object records the type argument it was **created** with. Filling a `List<dynamic>` with maps does not change its type argument. 3. **`as` checks the runtime type.** `x as List<Map<String, dynamic>>` asks whether the object is a list created with an element type that is a subtype of `Map<String, dynamic>`. A `List<dynamic>` is not, so the cast throws. 4. **Test literals differ.** A test that writes `{'items': [{'id': 'a1'}]}` as a Dart literal gets an inner list inferred from its elements, a `List<Map<String, String>>`, which is a subtype of `List<Map<String, dynamic>>`. The cast passes in the test and fails on real network data. ## The fixes compared | Approach | Behaviour | Use when | |---|---|---| | `[for (final e in raw) fromJsonT(e as Map<String, dynamic>)]` | converts eagerly; each element cast checked | you are building models anyway, the usual case | | `raw.cast<Map<String, dynamic>>()` | returns a **view**; each read checks the element and may throw later | you need a typed list quickly and trust the data | | `List<Map<String, dynamic>>.from(raw)` | **copies** and checks every element now | you want failures at the parse boundary | | `raw.whereType<Map<String, dynamic>>()` | **filters**, silently dropping mismatches | bad elements should be skipped, deliberately | `cast` has a subtlety: because it is a view, an error surfaces at the first read of a bad element, possibly far from the parsing code. For parsing, eager conversion gives better error locality. ## The generic part: passing the item parser A `Page<T>` cannot call `T.fromJson` because Dart does not allow static or constructor calls through a type parameter. The idiomatic signature takes a parser function: - `Page.fromJson(json, Order.fromJson)` passes a constructor tear-off; - the factory maps each raw element through it; - the result is a real `List<Order>`, so `page.items is List<Order>` is `true`. ## Maps have the same issue one level down The problem is not specific to lists. A decoded nested object is a `Map<String, dynamic>`, so casting `json['meta'] as Map<String, int>` fails even when every value is an integer. The same four tools exist for maps: `cast<K, V>()` returns a checking view, `Map<String, int>.from(raw)` copies and checks, and converting entry by entry gives the clearest errors. In practice the rule is the same at every level of the tree: **cast the container to its `dynamic` form, then convert each value into the type you actually want**. ## Hardening the boundary - Cast the outer value to `List<dynamic>` (or `List<Object?>`), never directly to a specific element type. - Validate each element's shape where you convert it, and wrap failures with context, such as which page and which index failed. - Decide explicitly between failing the whole page and skipping bad items; `whereType` skips silently, which can hide backend bugs. - Code generators for JSON models produce this same conversion code, including support for generic wrappers, which is one reason teams adopt them. ## Summary - Decoded JSON collections have `dynamic` type arguments, and reification keeps it that way. - `as List<X>` on them throws; convert elements instead. - Generic wrappers take a `T Function(Map<String, dynamic>)` rather than calling `T.fromJson`.

  • When is raw.cast<Map<String, dynamic>>() a poor choice for parsing?
    `cast` returns a lazy view that checks each element only when it is read. A malformed element then throws wherever the list is first read, perhaps in a widget build long after parsing, with no hint of which response caused it. Eager conversion or `List.from` fails at the parse boundary, where you can report the page and index.
  • Why can the same fromJson code pass a unit test and fail on real API data?
    A test that builds the input as a Dart literal gets a list whose type argument is inferred from its elements, such as `List<Map<String, String>>`, which already satisfies the cast. Real responses go through `jsonDecode`, which always creates `List<dynamic>`. Feeding tests the output of `jsonDecode` on a JSON string reproduces production behaviour.

saying these in an interview costs you the question

  • A List<dynamic> whose elements are all maps passes as List<Map<String, dynamic>>.
  • jsonDecode infers precise element types from the JSON content.
  • cast() copies the list and validates every element immediately.
  • whereType throws when an element has the wrong type.
  • Page<T>.fromJson can call T.fromJson on the type parameter.