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?
answer
- decoder builds List<dynamic>
- as checks the object's own type
- contents do not change the type argument
- cast view versus List.from copy
- map each element through fromJsonT
basics
~10 sjsonDecode 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 linesimport '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
Recall that decoded JSON lists are List<dynamic> and cannot be cast straight to a list of a specific type.
Explain how reification makes the runtime type argument fixed at creation, and compare cast, List.from, whereType and element-wise conversion.
Build parse boundaries that fail early with context, pass item parsers into generic wrappers, and test with jsonDecode output rather than Dart literals.
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.