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
}
If your Dart model uses a direct cast:
// CRASHES when price is serialized without decimals:
price: json["price"] as double?
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(),
);
}
}
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>)
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>),
);
}
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>?;
The Fix: For primitive lists, use .cast<T>() or List<T>.from():
tags: (json["tags"] as List<dynamic>?)?.cast<String>(),
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(),
4. The Tri-State Dilemma in PATCH Updates
In REST APIs, partial updates (PATCH) have three distinct states:
- Present with value (update field)
- Present as null (clear database value)
- 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;
}
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,
);
}
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,
);
}
Summary Checklist
To eliminate JSON-related crashes in Flutter apps:
- Parse numbers via
(json["key"] as num?)?.toDouble(). - Guard nested models with
json["obj"] == null ? null : .... - Map arrays via
(json["list"] as List<dynamic>?)?.map(...). - 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)