DEV Community

Rasika Dangamuwa
Rasika Dangamuwa

Posted on

Why Auto-Generated Dart Classes Crash Flutter Apps in Production: 5 JSON Deserialization Traps

You build a Flutter app against a staging API, verify the primary user flows, and release to app stores with zero compilation warnings. Two weeks later, crash analytics report spikes in production exceptions:

type 'Null' is not a subtype of type 'String'

type 'int' is not a subtype of type 'double' in type cast

type 'List<dynamic>' is not a subtype of type 'List<String>'

Why does this happen? In dynamic backends, JSON payloads deviate from static contracts. A price serialized as 19.99 becomes 20 when whole. An optional user address returns null instead of an object. A list arrives empty or contains mixed data.

Dart's sound null safety and runtime typing do not forgive these discrepancies. Here are five JSON deserialization traps in Flutter and Dart, and how to write resilient models.


1. The Integer vs. Double Runtime Cast Trap

In JSON, all numbers are numbers. Backends frequently omit trailing decimal zeros:

{
  "product_id": "SKU-902",
  "price": 25,
  "discount": 0.15
}
Enter fullscreen mode Exit fullscreen mode

If your Dart model uses a direct cast:

// CRASHES when price is serialized without decimals:
price: json["price"] as double?
Enter fullscreen mode Exit fullscreen mode

Dart throws type 'int' is not a subtype of type 'double'. Because int is not a subtype of double (both extend num), this cast fails immediately.

The Fix: Cast through num? and call .toDouble():

class Product {
  final String? productId;
  final double? price;

  Product({this.productId, this.price});

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      productId: json["product_id"] as String?,
      price: (json["price"] as num?)?.toDouble(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Nested Object Null Safety Propagation

When parsing nested JSON objects (e.g. shipping addresses or metadata), naive code often casts the map directly:

// Unsafe: crashes if address is null
address: Address.fromJson(json["address"] as Map<String, dynamic>)
Enter fullscreen mode Exit fullscreen mode

If the API returns "address": null, casting Null to Map<String, dynamic> triggers a runtime crash before fromJson is called.

The Fix: Guard nested objects with an explicit null check:

factory UserProfile.fromJson(Map<String, dynamic> json) {
  return UserProfile(
    id: json["id"] as String?,
    address: json["address"] == null
        ? null
        : Address.fromJson(json["address"] as Map<String, dynamic>),
  );
}
Enter fullscreen mode Exit fullscreen mode

When building models across dozens of endpoints, writing boilerplate by hand invites edge-case bugs. Browser utilities like Nutilz JSON to Dart automate these null guards, generating safe == null ? null : ... factories and json_serializable annotations entirely client-side without uploading private API payloads.


3. Runtime List Casting and Type Erasure

When Dart decodes JSON via jsonDecode(), arrays become List<dynamic>. A direct cast throws at runtime:

// CRASH: type 'List<dynamic>' is not a subtype of type 'List<String>'
final List<String>? tags = json["tags"] as List<String>?;
Enter fullscreen mode Exit fullscreen mode

The Fix: For primitive lists, use .cast<T>() or List<T>.from():

tags: (json["tags"] as List<dynamic>?)?.cast<String>(),
Enter fullscreen mode Exit fullscreen mode

For lists of nested models, map each item safely:

orders: (json["orders"] as List<dynamic>?)
    ?.map((e) => OrderItem.fromJson(e as Map<String, dynamic>))
    .toList(),
Enter fullscreen mode Exit fullscreen mode

4. The Tri-State Dilemma in PATCH Updates

In REST APIs, partial updates (PATCH) have three distinct states:

  1. Present with value (update field)
  2. Present as null (clear database value)
  3. Absent / omitted (preserve current value)

Simple nullable fields (final String? bio;) conflate absence with explicit null. When serializing back to JSON with toJson(), sending "bio": null may unintentionally erase database fields.

The Fix: Omit unchanged keys during serialization:

Map<String, dynamic> toJson() {
  final data = <String, dynamic>{};
  if (productId != null) data["product_id"] = productId;
  if (price != null) data["price"] = price;
  return data;
}
Enter fullscreen mode Exit fullscreen mode

5. The Immutable copyWith() Null Ambiguity

Flutter state management architectures (Bloc, Riverpod) rely on immutable models and copyWith():

User copyWith({String? name, String? bio}) {
  return User(
    name: name ?? this.name,
    bio: bio ?? this.bio,
  );
}
Enter fullscreen mode Exit fullscreen mode

Passing bio: null evaluates to this.bio, making it impossible to reset bio back to null.

The Fix: Use a value getter callback if fields require explicit null resets:

User copyWith({
  String? name,
  String? Function()? bio,
}) {
  return User(
    name: name ?? this.name,
    bio: bio != null ? bio() : this.bio,
  );
}
Enter fullscreen mode Exit fullscreen mode

Summary Checklist

To eliminate JSON-related crashes in Flutter apps:

  1. Parse numbers via (json["key"] as num?)?.toDouble().
  2. Guard nested models with json["obj"] == null ? null : ....
  3. Map arrays via (json["list"] as List<dynamic>?)?.map(...).
  4. Default incoming API fields to nullable (?).

When bootstrapping new models from API schemas, running raw payloads through the Nutilz JSON to Dart tool generates production-ready Dart classes with safe null handling and toJson methods in seconds.

Top comments (0)