Fix: type 'Null' is not a subtype of type 'X' in Dart
This runtime error means a null reached a cast expecting a non-nullable type, almost always from JSON. Make the field nullable, or supply a default before the cast runs.
type 'Null' is not a subtype of type 'X' is a runtime type error: a null reached a spot in your code that a cast or an assignment insisted was non-nullable. The overwhelmingly common source is JSON parsing, where a key is missing or arrives as null and you cast it straight to String, int, or a model type. The fix is to stop the cast from seeing a raw null: either declare the target type nullable (String?) and handle the null, or supply a default with ?? fallback before the cast happens. This is verified against Dart 3.12 (Flutter 3.44); the behaviour has been the same for every sound-null-safety release since Dart 2.12.
The error in context
The message names the concrete type the value was supposed to be. From a JSON decode it usually looks like this:
Unhandled Exception: type 'Null' is not a subtype of type 'String' in type cast
#0 _$UserFromJson (package:myapp/models/user.dart:12:34)
#1 new User.fromJson (package:myapp/models/user.dart:8:7)
#2 fetchUser (package:myapp/api/client.dart:41:24)
<asynchronous suspension>
Two words in that message do all the work. The first, 'Null', is the type the value actually had at runtime: it was null. The second, after “subtype of type”, is what the code demanded: 'String', 'int', 'List<dynamic>', 'Map<String, dynamic>', or one of your own model classes. The trailing in type cast tells you the failure happened at an explicit or implicit as cast, which is the tell-tale fingerprint of decoding untyped dynamic JSON into typed fields.
You will also see the variant without in type cast, for example type 'Null' is not a subtype of type 'String', when the value flows into a non-nullable parameter or field rather than an as expression. Same root cause, same fixes.
Why this happens
Under sound null safety, Null is its own type and is not a subtype of any non-nullable type. That is the entire point of null safety: String genuinely cannot hold null, so the runtime refuses to let a null masquerade as one. When you write json['name'] as String and json['name'] is null, you are asking the runtime to treat Null as String, and it throws.
The reason this shows up at runtime rather than compile time is that JSON is dynamic. jsonDecode returns dynamic, and every lookup on a Map<String, dynamic> is also dynamic. The compiler cannot see what is actually in the map, so it trusts your as String cast and defers the check to runtime. If the real value is null, the check fails the moment that line executes. This is why the error is so common in fromJson factories and in code generated by json_serializable: those are exactly the places where dynamic values get forced into typed shapes.
There are three situations that produce it, in rough order of frequency:
- The JSON key is missing entirely, so
json['key']returnsnull, and you cast thatnullto a non-nullable type. - The key is present but its value is JSON
null, for example a nullable column serialized from a backend. - The value is present but the wrong shape, for example a number arriving as
intwhen you cast toString, or an object where you expected a list. This throws a different subtype message but is the same category of bug.
Minimal repro
The smallest snippet that reproduces the canonical case:
// Dart 3.12, Flutter 3.44
import 'dart:convert';
class User {
final String name;
final int age;
User({required this.name, required this.age});
factory User.fromJson(Map<String, dynamic> json) => User(
name: json['name'] as String, // throws if 'name' is null or missing
age: json['age'] as int,
);
}
void main() {
// 'name' is absent from the payload
final payload = jsonDecode('{"age": 30}') as Map<String, dynamic>;
final user = User.fromJson(payload); // type 'Null' is not a subtype of type 'String'
print(user.name);
}
json['name'] evaluates to null because the key is not in the map. The as String cast then tries to view null as a String and throws. Note that the exception is raised inside User.fromJson, not at the print, which is why the stack trace points at your model file and not at the widget that ultimately displayed the data.
Fix, in detail
Work through these in order. The first two cover almost every real occurrence; the rest handle the shapes the simple fixes do not.
1. Make the field nullable if the data really can be absent
If the backend can legitimately omit or null the value, model that honestly. Declare the Dart field nullable and let the cast target a nullable type, which null is a subtype of:
// Dart 3.12, Flutter 3.44
class User {
final String? name; // was String
final int age;
User({this.name, required this.age});
factory User.fromJson(Map<String, dynamic> json) => User(
name: json['name'] as String?, // as String?, not as String
age: json['age'] as int,
);
}
json['name'] as String? succeeds whether the value is a String or null, because Null is a subtype of String?. The trade is that every consumer of name now has to handle the null, which is exactly the correctness the type system is asking you to acknowledge. This is the right fix when the field is genuinely optional.
2. Supply a default with ?? before the value reaches a non-nullable field
If the field must stay non-nullable but you can pick a sensible fallback, coalesce the null away before the cast is complete:
// Dart 3.12, Flutter 3.44
factory User.fromJson(Map<String, dynamic> json) => User(
name: json['name'] as String? ?? 'Unknown', // cast to nullable, then default
age: json['age'] as int? ?? 0,
);
The ordering matters. Cast to the nullable type first (as String?), then apply ??. If you write json['name'] ?? 'Unknown' as String, precedence makes it json['name'] ?? ('Unknown' as String), which does not protect the left side and still throws when the value is a wrong non-null type. Casting to nullable and coalescing is the idiom that reads cleanly and behaves correctly.
3. Never cast a dynamic map value straight to a non-nullable type
The habit that causes this error is json['x'] as ConcreteType. Make the safe form your default: cast to the nullable type, then decide what a null means. For nested objects and lists, the same rule applies one level deeper:
// Dart 3.12, Flutter 3.44
// A list that may be absent -> default to empty, never null-cast the elements
final tags = (json['tags'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
<String>[];
// A nested object that may be absent -> guard before recursing
final address = json['address'] == null
? null
: Address.fromJson(json['address'] as Map<String, dynamic>);
Casting the outer container to List<dynamic>? or checking == null before recursing stops the null from ever reaching an element or nested cast. This is where hand-written fromJson code most often goes wrong: the top-level field is guarded but the list elements or the nested map are not.
4. If you use json_serializable, mark the field nullable or give it a default
Generated fromJson code casts exactly the way you would by hand, so a non-nullable field with @JsonKey produces the same runtime error when the data is missing. Fix it at the model declaration and regenerate:
// Dart 3.12, Flutter 3.44, json_serializable 6.x
@JsonSerializable()
class User {
final String? name; // nullable -> generator emits `as String?`
@JsonKey(defaultValue: 0) final int age; // default -> used when the key is null/absent
User({this.name, required this.age});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
A nullable field makes the generator emit as String?. A @JsonKey(defaultValue: ...) makes it substitute the default when the key is absent or null. Run dart run build_runner build --delete-conflicting-outputs after changing the annotations, otherwise the old generated cast is still what runs.
5. Fix the shape mismatch when the value is not actually null
If the error names a type like 'String' but the payload clearly has a value, the value is the wrong shape. A backend that sends "age": "30" (string) when you cast as int, or "30" where you expect a number, triggers the same family of error. Coerce explicitly instead of casting:
// Dart 3.12, Flutter 3.44
// Backend sends age as a string sometimes, an int other times
final age = json['age'] is int
? json['age'] as int
: int.parse(json['age'].toString());
This is not the Null case, but it lands people on this page because the message shape is identical. When the type in the message is not 'Null' on the left, look at what the server actually sent, not at your null handling.
Gotchas and variants
-
The stack trace points at the model, not the UI. Because the cast runs inside
fromJson, the top frame is your model file. Developers often start debugging the widget that showed the blank field; the real fix is one or two frames down, at the cast. Read the first non-framework frame in the trace. -
as String?is not the same asas String. The single?is the whole fix in most cases.as String?permitsnull;as Stringforbids it. If you copy a cast from a non-nullable field to a nullable one, remember to add the?, or you have moved the bug, not fixed it. -
Map<String, dynamic>casts fail the same way.jsonDecodereturnsdynamic. If the whole payload isnull(an empty 204 response body, for example) thenjsonDecode(body) as Map<String, dynamic>throwstype 'Null' is not a subtype of type 'Map<String, dynamic>'before you even reach a field. Guard the decode:body.isEmpty ? null : jsonDecode(body). This overlaps with malformed input handled in FormatException: Unexpected character when parsing JSON in Dart. -
The bang operator moves the crash, it does not fix it. Writing
json['name']!converts aNullsubtype error intoNull check operator used on a null value. It is the same underlying null, thrown by a different mechanism. See Null check operator used on a null value in Flutter for why!should be reserved for values you can prove are non-null, not used to silence a compiler you disagree with. -
latefields turn the same null into a different error. If you route a possibly-null value into alatenon-nullable field and read it before assignment, you getLateInitializationErrorinstead. The cure is the same: model the absence honestly rather than promising a value you do not have. See LateInitializationError: Field has not been initialized in Flutter. -
Generic type arguments hide the cast.
List<String>.from(json['tags'])and(json['tags'] as List).cast<String>()both throw this error lazily, when an element isnull, and the trace can point at.castrather than your code. Prefer an explicit.map((e) => e as String?)so the failure is visible and yours to handle. -
This is a runtime error, so tests catch it and the analyzer does not. The analyzer cannot see inside a
dynamicJSON map, sodart analyzestays green while the cast is unsafe. A singlefromJsonunit test with a payload that omits the field surfaces the bug before a user does. If you are migrating an older codebase, the Flutter null safety migration checklist walks through where these casts tend to hide.
The mental model to carry away: this error is null safety refusing to let a null pretend to be something it is not. The value came in as null, and some cast or assignment downstream promised it would not be. The fix is never to force the cast harder; it is to decide, at the boundary where the dynamic data enters your typed world, whether that field can be absent. If it can, make it nullable and handle the null. If it cannot, give it a default before the cast completes. Do that at every json[...] lookup and this error stops appearing.
Related
- Fix: FormatException: Unexpected character when parsing JSON in Dart for the sibling error when the payload is not even valid JSON.
- Fix: Null check operator used on a null value in Flutter for what happens when you reach for
!to silence this cast instead. - Fix: LateInitializationError: Field has not been initialized in Flutter for the
latevariant of promising a value you do not have. - Migrate a Flutter 2 app to Flutter 3.x: null safety checklist for finding these unsafe casts across a whole codebase.
Sources
- Dart, Understanding null safety (why
Nullis no longer a subtype of every type, and why implicit downcasts fromdynamicbecame explicit casts that fail at runtime). - Dart, Sound null safety (the guarantees that make a non-nullable
Stringrejectnullat runtime). - GitHub, dart-lang/sdk issue #53700 (“type ‘Null’ is not a subtype of type ‘String’” reported against real JSON-decoding code, with the missing-key root cause).
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.