
Flutter: Debugging JSON Parsing Errors
App crashed with TypeError? Learn why 'Null is not a subtype of String' happens and how to make your JSON parsing bulletproof with Zod/Freezed.

App crashed with TypeError? Learn why 'Null is not a subtype of String' happens and how to make your JSON parsing bulletproof with Zod/Freezed.
Yellow stripes appear when the keyboard pops up? Learn how to handle layout overflows using resizeToAvoidBottomInset, SingleChildScrollView, and tricks for chat apps.

Push works on Android but silent on iOS? Learn to fix APNs certificates, handle background messages, configure Notification Channels, and debug FCM integration errors.

Class is there, but style is missing? Debugging Tailwind CSS like a detective.

Think Android is easier than iOS? Meet Gradle Hell. Learn to fix minSdkVersion conflicts, Multidex limit errors, Namespace issues in Gradle 8.0, and master dependency analysis with `./gradlew dependencies`.

The server dev said, "I updated the API." Then my app died with the Red Screen of Death.
type 'Null' is not a subtype of type 'String' in type cast
Every Flutter dev has seen this in their nightmares. The culprit is 99% a JSON Parsing issue involving Null.
"You said String is required!"
Ideally, yes. But defensively, I was wrong not to clean the data. If the server sends null, my app shouldn't commit suicide.
Since Dart 2.12 Null Safety, variables are non-nullable by default.
String name; // Can NEVER be null
String? bio; // Can be null
JSON is dynamic runtime data (Map<String, dynamic>).
The compiler doesn't know if json['name'] is String or null.
When you cast it as String or pass it to a required constructor, it explodes at runtime if the value turns out to be null.
Don't trust API docs 100%. "Required" fields can be null due to server bugs or DB migrations.
The Client must be Crash-free against garbage data.
// ❌ Dangerous: Explicit casting assumes safety
class User {
final String name; // Crashes if null
factory User.fromJson(Map<String, dynamic> json) {
return User(name: json['name']);
}
}
// ✅ Safe: Implicit safety
class User {
final String name;
factory User.fromJson(Map<String, dynamic> json) {
// Fallback to default if null
return User(name: json['name'] ?? 'Unknown');
}
}
The ?? operator (Null-aware operator) is your savior.
If json['name'] is null, it uses 'Unknown'. The app lives on.
If loading a list of 100 items, and 1 has malformed data, don't fail the whole list. Filter out the bad ones using a try-catch block inside the mapping function.
List<User> parseUsers(List<dynamic> jsonList) {
return jsonList.map((json) {
try {
return User.fromJson(json);
} catch (e) {
print('Bad data ignored: $json');
return null; // Return null on failure
}
}).whereType<User>() // Remove nulls from the stream
.toList();
}
Now the app survives even if the server sends one user with name: null while others are fine.
freezedHand-coding fromJson is prone to typos.
Use freezed + json_serializable.
The @Default annotation makes handling nulls trivial and readable.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
factory User({
@Default('Unknown') String name, // Auto-fills if null
@Default(0) int age, // Default to 0 if null
@Default([]) List<String> tags, // Default to empty list
}) = _User;
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}
This generates defensive code automatically. You will rarely see a parsing error again.
Be careful with 64-bit Integers (e.g., Database IDs, Twitter Snowflakes). Dart (when compiled to Web/JS) and JavaScript only support safe integers up to $2^-1$ (approx 9 quadrillion). If the ID is larger, the last digits will get corrupted (rounding error).
Best Practice: Ask the backend to send IDs as Strings. If you must handle numbers, implement a Custom JsonConverter to handle the type casting safely.
class StringOrIntConverter implements JsonConverter<String, dynamic> {
const StringOrIntConverter();
@override
String fromJson(dynamic json) {
// Handle both String and Int from server
return json.toString();
}
@override
dynamic toJson(String object) => object;
}
This flexibility protects your app from silent data corruption when the backend decides to change the ID type without notice.
If you are using React/TypeScript, types like interface User vanish at runtime. They don't protect you from bad JSON.
Use Zod for "Runtime Validation".
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string().default("Guest"), // Fallback
age: z.number().optional(), // Nullable
});
// Safe Parse
const result = UserSchema.safeParse(response.data);
if (!result.success) {
// Handle error gracefully instead of crashing
console.error(result.error);
} else {
// result.data is typed AND validated
console.log(result.data.name);
}
Zod ensures that the data entering your application boundaries matches your expectations before it hits your UI components. It acts as a gatekeeper.
I learned this the hard way.
I was building an e-commerce app. The backend promised to send price as an Int.
So I coded final int price = json['price'];.
On Black Friday, the server team deployed a "10% Discount Feature".
Suddenly, prices became floats (49.99 instead of 50).
The exact JSON payload was {"price": 49.99}.
My app, expecting an Int, crashed on the "Product List Page".
Users couldn't see any products. Sales dropped to zero for 2 hours until I hotfixed it.
Lesson: For money, always use num (covers both int and double) or String.
Never assume types are immutable. The server can (and will) change them.
?.json_serializable that intercepts the parsing logic to transform data (e.g., String -> Date).Q: type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'List<dynamic>'
A: You are expecting a List [], but the server sent a Map {}. Check if the response is wrapped in a data field (e.g., { "data": [...] }).
Q: How to handle generic types Result<T>?
A: freezed supports Generics. You can define factory Result.fromJson(Map<String, Object?> json, T Function(Object?) fromJsonT). It requires passing a deserializer function for T.
Q: Is dynamic dangerous?
A: Yes. dynamic disables the type checker. Avoid using it unless absolutely necessary. Prefer Object? if the type is unknown, forcing you to check types before usage.
?? to provide defaults.whereType) instead of crashing the whole list.freezed (Dart) or Zod (TS) to automate validation.A slightly empty UI (missing name) is infinitely better than a Crashed App.