DEV Community

Chanpenh Yorn
Chanpenh Yorn

Posted on

Why your Flutter JSON model crashes at runtime (and how to generate one that doesn't)

You write a model, the first API response parses fine, and a week later a crash report says:

type 'int' is not a subtype of type 'double' in type cast
Enter fullscreen mode Exit fullscreen mode

Nothing in your code changed. The data did. Here are the three type errors I see most often with jsonDecode, why they happen, and how to write (or generate) models that survive real responses.

1. int vs double

Dart's jsonDecode reads the number type from the text. 1.0 is a double, 1 is an int:

import 'dart:convert';

void main() {
  final a = jsonDecode('{"price": 1.0}') as Map<String, dynamic>;
  print(a['price'].runtimeType); // double

  final b = jsonDecode('{"price": 1}') as Map<String, dynamic>;
  final price = b['price'] as double;
  // type 'int' is not a subtype of type 'double' in type cast
}
Enter fullscreen mode Exit fullscreen mode

Many backends drop the .0 for whole numbers, so a price of 3.0 arrives as 3. The safe cast for any field that is "a number with decimals" is:

final price = (json['price'] as num).toDouble();
Enter fullscreen mode Exit fullscreen mode

2. List<dynamic> vs List<String>

jsonDecode has no idea what your list should contain, so every JSON array becomes List<dynamic>:

final json = jsonDecode('{"tags": ["a", "b"]}') as Map<String, dynamic>;
final tags = json['tags'] as List<String>;
// type 'List<dynamic>' is not a subtype of type 'List<String>' in type cast
Enter fullscreen mode Exit fullscreen mode

Cast each item instead of the list:

final tags = (json['tags'] as List<dynamic>).map((e) => e as String).toList();
Enter fullscreen mode Exit fullscreen mode

Lists of objects work the same way, with fromJson on each item:

final items = (json['items'] as List<dynamic>)
    .map((e) => Item.fromJson(e as Map<String, dynamic>))
    .toList();
Enter fullscreen mode Exit fullscreen mode

3. The key that is only sometimes there

You build the model from one sample response. In that sample every item has a note. In production some items don't, and json['note'] as String throws because the value is null. Any key that can be missing or null needs a nullable type, String?, and a non-required constructor parameter.

The fix is to look at more than one sample. If a key is missing from any item, or null in any item, it's nullable.

A model that handles all three

Given this sample, where the second item has a note and the first doesn't:

{
  "id": 7,
  "price": 1.0,
  "tags": ["new"],
  "items": [
    { "sku": "A1", "qty": 2 },
    { "sku": "B2", "qty": 1, "note": "gift" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

a model that survives real responses looks like this:

class Order {
  final int id;
  final double price;
  final List<String> tags;
  final List<Item> items;

  const Order({
    required this.id,
    required this.price,
    required this.tags,
    required this.items,
  });

  factory Order.fromJson(Map<String, dynamic> json) => Order(
        id: json['id'] as int,
        price: (json['price'] as num).toDouble(),
        tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
        items: (json['items'] as List<dynamic>).map((e) => Item.fromJson(e as Map<String, dynamic>)).toList(),
      );

  Map<String, dynamic> toJson() => {
        'id': id,
        'price': price,
        'tags': tags,
        'items': items.map((e) => e.toJson()).toList(),
      };
}

class Item {
  final String sku;
  final int qty;
  final String? note;

  const Item({
    required this.sku,
    required this.qty,
    this.note,
  });

  factory Item.fromJson(Map<String, dynamic> json) => Item(
        sku: json['sku'] as String,
        qty: json['qty'] as int,
        note: json['note'] as String?,
      );

  Map<String, dynamic> toJson() => {
        'sku': sku,
        'qty': qty,
        'note': note,
      };
}
Enter fullscreen mode Exit fullscreen mode

Decoding {"id": 8, "price": 3, "tags": [], "items": [{"sku": "C3", "qty": 1}]} with it gives price = 3.0 and note = null, with no exceptions.

Generating it

I wrote that model by hand far too many times, so I built a free JSON to Dart generator (disclosure: it's my project). The model above is its actual output for the sample. It:

  • runs entirely in the browser, so API responses are never uploaded,
  • reads number types from the raw text, so 1.0 stays a double (JavaScript's JSON.parse would turn it into 1),
  • merges the keys of every item in an array and makes keys that are missing or null in some items nullable,
  • outputs plain Dart (no packages), json_serializable or freezed,
  • can repair a map printed by print() / debugPrint (like {id: 1, name: Ada}) into JSON first.

It does not guess DateTime or enums; dates stay String, so convert those by hand.

If you're debugging one of these errors right now, the guide Flutter JSON Errors: "is not a subtype" Explained covers more of them, including '_Map<String, dynamic>' is not a subtype of type ....

Top comments (0)