Start Debugging

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:

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 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.

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back