jsonDecode returns dynamic. Dart only checks the real type when your code casts or assigns that value, so a mismatch between your model and the actual API response shows up as a _TypeError at runtime, not as a compile error. This guide lists the messages you are most likely to see, what each one means and how to fix it.
Every message below was produced by running the code on Dart 3.13.5 (the Dart VM, which is what Flutter uses on Android, iOS and desktop in debug mode). Flutter web compiles to JavaScript and may word the same errors differently. Older SDKs printed _InternalLinkedHashMap<String, dynamic> where newer ones print _Map<String, dynamic> [not re-tested here]; the cause and fix are the same.
Quick lookup
| Message | Usual cause |
|---|---|
'_Map<String, dynamic>' is not a subtype of type 'List<dynamic>' | The response is an object; the list is inside a key |
'List<dynamic>' is not a subtype of type 'Map<String, dynamic>' | The response is a list, not a single object |
'List<dynamic>' is not a subtype of type 'List<String>' | Casting a decoded list directly to a typed list |
'int' is not a subtype of type 'double' | A whole number such as 10 in a double field |
'Null' is not a subtype of type 'String' | A missing key or a JSON null in a non-nullable field |
'String' is not a subtype of type 'int' | The API sends numbers as strings, such as "42" |
'String' is not a subtype of type 'Map<String, dynamic>' | The body was not decoded, or was encoded twice |
FormatException: Unexpected character | The text is not JSON: HTML, a Dart Map.toString() log or an empty body |
The fastest way to find which case you have is to look at the real response. Print response.body, paste it into the JSON formatter and compare its shape with what your model expects.
1. '_Map<String, dynamic>' is not a subtype of type 'List<dynamic>'
final List<dynamic> users = jsonDecode(response.body);
// _TypeError: type '_Map<String, dynamic>' is not a subtype of type 'List<dynamic>'Your code expects a JSON array, but the API returned an object. Many APIs wrap lists in an envelope such as {"data": [...], "total": 42}. Decode the object first, then read the list from its key:
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final users = (decoded['data'] as List<dynamic>)
.map((e) => User.fromJson(e as Map<String, dynamic>))
.toList();2. 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>'
final Map<String, dynamic> user = jsonDecode(response.body);
// _TypeError: type 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>'The opposite case: the endpoint returns an array (even one with a single item) and the code expects one object. Decode as a list and map each item, or take the first item if that is really what you want:
final list = jsonDecode(response.body) as List<dynamic>;
final first = list.first as Map<String, dynamic>;3. 'List<dynamic>' is not a subtype of type 'List<String>'
final tags = json['tags'] as List<String>;
// _TypeError: type 'List<dynamic>' is not a subtype of type 'List<String>' in type castjsonDecode always creates List<dynamic>, even when every element is a string. A cast with as checks the list's own type, not its contents, so it fails. Convert the list instead:
// Copies the list and checks every element now.
final tags = List<String>.from(json['tags'] as List);
// A lazy view: a wrong element only fails when it is read.
final tags2 = (json['tags'] as List<dynamic>).cast<String>();The same rule applies to lists of objects. Assigning to List<Map<String, dynamic>> fails with type 'List<dynamic>' is not a subtype of type 'List<Map<String, dynamic>>'; use .cast<Map<String, dynamic>>() or map each item through fromJson.
4. 'int' is not a subtype of type 'double'
final price = json['price'] as double; // JSON: {"price": 10}
// _TypeError: type 'int' is not a subtype of type 'double' in type castJSON has a single number type. Dart decodes 10 as an int and 10.5 as a double, so a field that is usually fractional breaks the first time the server sends a whole number. Cast to num and convert:
final price = (json['price'] as num).toDouble();5. 'Null' is not a subtype of type 'String'
final name = json['name'] as String; // JSON: {"id": 1}
// _TypeError: type 'Null' is not a subtype of type 'String' in type castReading a key that is missing from a Map returns null, and so does a key whose value is JSON null. Decide which fields can really be absent, make them nullable in the model, or supply a default:
final email = json['email'] as String?; // nullable field
final name = json['name'] as String? ?? ''; // default valueIf the field should always be present, a default can hide a backend bug. In that case keep the strict cast and fix the data, or throw an error that names the field.
6. 'String' is not a subtype of type 'int'
final id = json['id'] as int; // JSON: {"id": "42"}
// _TypeError: type 'String' is not a subtype of type 'int' in type castSome APIs send IDs and amounts as strings, often to avoid precision loss in JavaScript clients. If the API does this consistently, model the field as a String. If it mixes both forms, accept both:
final raw = json['id'];
final id = raw is String ? int.parse(raw) : raw as int;Use int.tryParse instead of int.parse when an invalid value should become null rather than throw a FormatException.
7. Map<String, dynamic> vs Map<String, String>
final headers = json['headers'] as Map<String, String>;
// _TypeError: type '_Map<String, dynamic>' is not a subtype of type 'Map<String, String>' in type castNested objects are decoded as Map<String, dynamic>, just like lists are List<dynamic>. Copy the map into the type you need:
final headers = Map<String, String>.from(json['headers'] as Map);8. 'String' is not a subtype of type 'Map<String, dynamic>'
You passed a string where a decoded map was expected. There are two common causes:
- Calling
User.fromJson(response.body)withoutjsonDecodefirst. - Double-encoded JSON: the server returned a JSON string that itself contains JSON, such as
"{\"id\": 1}". OnejsonDecodecall gives you aString, not a map.
The real fix for double encoding is on the server. As a client-side workaround, decode a second time when the first result is a string:
Object? value = jsonDecode(response.body);
if (value is String) value = jsonDecode(value);
final json = value as Map<String, dynamic>;9. FormatException: Unexpected character
A FormatException comes from jsonDecode itself and means the text is not valid JSON. The message shows the input and a caret under the problem. Three inputs cause most of them:
jsonDecode('<!DOCTYPE html><html>');
// FormatException: Unexpected character (at character 1)
jsonDecode('{name: Ada, id: 1}');
// FormatException: Unexpected character (at character 2)
jsonDecode('');
// FormatException: Unexpected end of input (at character 1)- HTML at character 1 means the server returned an error page, a login redirect or a proxy message. Check
response.statusCodebefore decoding. {name: Ada, id: 1}is whatprint(map)outputs. It is Dart'sMap.toString(), not JSON. UsejsonEncode(map)when you log data you want to reuse. If all you have is the log line, the JSON formatter's repair option converts Dart map output back into JSON in your browser. Check the result: unquoted values that look like numbers ortrue/falsebecome those types.- An empty body usually comes with a
204 No Contentstatus or a failed request. Check for an empty body before decoding.
For other syntax problems, such as trailing commas or single quotes, see common JSON errors.
Preventing these errors
- Dates arrive as strings or Unix numbers, never as
DateTime. See parse DateTime from JSON forFormatException: Invalid date formatand timestamps that land in 1970. - Generate models from a real response rather than writing casts by hand. The JSON to Dart generator builds typed lists, makes fields nullable when they are missing or null in some items, reads decimals with
(… as num).toDouble(), and runs in your browser. - Use Dart 3 pattern matching to check the shape and types in one step, with no cast that can throw:
if (json case {'id': int id, 'name': String name}) { return User(id: id, name: name); } throw FormatException('Unexpected user JSON: $json'); - When an endpoint starts failing after a backend change, save an old and a new response and compare them with the JSON diff tool. Type changes such as
10→"10"are listed separately from value changes. - For the wider debugging workflow (status codes, headers, auth), see how to debug API responses.