Skip to content
DevPenh

Flutter JSON Errors: "is not a subtype" Explained

Fix Flutter and Dart JSON errors: List<dynamic> is not a subtype of Map<String, dynamic>, int is not a subtype of double, Null errors and FormatException.

By Yorn Chanpenh · Last reviewed

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

MessageUsual 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 characterThe 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 cast

jsonDecode 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 cast

JSON 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 cast

Reading 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 value

If 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 cast

Some 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 cast

Nested 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) without jsonDecode first.
  • Double-encoded JSON: the server returned a JSON string that itself contains JSON, such as "{\"id\": 1}". One jsonDecode call gives you a String, 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.statusCode before decoding.
  • {name: Ada, id: 1} is what print(map) outputs. It is Dart's Map.toString(), not JSON. Use jsonEncode(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 or true/false become those types.
  • An empty body usually comes with a 204 No Content status 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 for FormatException: Invalid date format and 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.

Part of the API debugging series. Start with How to Debug API Responses: A Practical Workflow.