Start Debugging

Solución: type 'Null' is not a subtype of type 'X' en Dart

Este error de ejecución significa que un null llegó a una conversión que esperaba un tipo no anulable, casi siempre desde JSON. Haz el campo anulable, o proporciona un valor por defecto antes de la conversión.

type 'Null' is not a subtype of type 'X' es un error de tipo en tiempo de ejecución: un null llegó a un punto de tu código donde una conversión o una asignación insistía en un tipo no anulable. La fuente abrumadoramente común es el análisis de JSON, donde una clave falta o llega como null y tú la conviertes directamente a String, int o un tipo de modelo. La solución es impedir que la conversión vea un null crudo: o declaras el tipo de destino como anulable (String?) y manejas el null, o proporcionas un valor por defecto con ?? fallback antes de que la conversión ocurra. Esto está verificado contra Dart 3.12 (Flutter 3.44); el comportamiento ha sido el mismo en cada versión con null safety sólido desde Dart 2.12.

El error en contexto

El mensaje nombra el tipo concreto que se suponía que el valor debía ser. Desde una decodificación de JSON normalmente se ve así:

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>

Dos palabras en ese mensaje hacen todo el trabajo. La primera, 'Null', es el tipo que el valor realmente tenía en tiempo de ejecución: era null. La segunda, tras “subtype of type”, es lo que el código exigía: 'String', 'int', 'List<dynamic>', 'Map<String, dynamic>' o una de tus propias clases de modelo. El in type cast final te dice que el fallo ocurrió en una conversión as explícita o implícita, que es la huella reveladora de decodificar JSON dynamic sin tipar hacia campos tipados.

También verás la variante sin in type cast, por ejemplo type 'Null' is not a subtype of type 'String', cuando el valor fluye hacia un parámetro o campo no anulable en lugar de una expresión as. La misma causa raíz, las mismas soluciones.

Por qué sucede

Bajo null safety sólido, Null es su propio tipo y no es un subtipo de ningún tipo no anulable. Ese es todo el sentido del null safety: String genuinamente no puede contener null, así que el runtime se niega a dejar que un null se haga pasar por uno. Cuando escribes json['name'] as String y json['name'] es null, estás pidiendo al runtime que trate Null como String, y lanza el error.

La razón por la que esto aparece en tiempo de ejecución en lugar de en tiempo de compilación es que JSON es dynamic. jsonDecode devuelve dynamic, y cada búsqueda en un Map<String, dynamic> también es dynamic. El compilador no puede ver qué hay realmente en el mapa, así que confía en tu conversión as String y difiere la comprobación al tiempo de ejecución. Si el valor real es null, la comprobación falla en el momento en que esa línea se ejecuta. Por eso el error es tan común en las fábricas fromJson y en el código generado por json_serializable: esos son exactamente los lugares donde los valores dynamic se fuerzan a formas tipadas.

Hay tres situaciones que lo producen, en orden aproximado de frecuencia:

Reproducción mínima

El fragmento más pequeño que reproduce el caso canónico:

// 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'] se evalúa como null porque la clave no está en el mapa. La conversión as String entonces intenta ver null como un String y lanza el error. Ten en cuenta que la excepción se lanza dentro de User.fromJson, no en el print, razón por la cual la traza de pila apunta a tu archivo de modelo y no al widget que finalmente mostró los datos.

Solución, en detalle

Recorre estas en orden. Las dos primeras cubren casi cada caso real; el resto maneja las formas que las soluciones simples no cubren.

1. Haz el campo anulable si los datos realmente pueden faltar

Si el backend puede omitir o anular legítimamente el valor, modélalo con honestidad. Declara el campo de Dart como anulable y deja que la conversión apunte a un tipo anulable, del cual null es subtipo:

// 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? tiene éxito tanto si el valor es un String como si es null, porque Null es un subtipo de String?. El compromiso es que cada consumidor de name ahora tiene que manejar el null, que es exactamente la corrección que el sistema de tipos te está pidiendo que reconozcas. Esta es la solución correcta cuando el campo es genuinamente opcional.

2. Proporciona un valor por defecto con ?? antes de que el valor llegue a un campo no anulable

Si el campo debe permanecer no anulable pero puedes elegir un valor por defecto sensato, elimina el null antes de que la conversión se 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,
    );

El orden importa. Convierte primero al tipo anulable (as String?), luego aplica ??. Si escribes json['name'] ?? 'Unknown' as String, la precedencia lo convierte en json['name'] ?? ('Unknown' as String), que no protege el lado izquierdo y aún lanza el error cuando el valor es un tipo no nulo equivocado. Convertir a anulable y aplicar la fusión de nulos es el idioma que se lee limpiamente y se comporta correctamente.

3. Nunca conviertas el valor de un mapa dynamic directamente a un tipo no anulable

El hábito que causa este error es json['x'] as ConcreteType. Haz de la forma segura tu opción por defecto: convierte al tipo anulable, luego decide qué significa un null. Para objetos y listas anidados, se aplica la misma regla un nivel más profundo:

// 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>);

Convertir el contenedor externo a List<dynamic>? o comprobar == null antes de recurrir impide que el null llegue jamás a un elemento o a una conversión anidada. Aquí es donde el código fromJson escrito a mano más a menudo se equivoca: el campo de nivel superior está protegido pero los elementos de la lista o el mapa anidado no lo están.

4. Si usas json_serializable, marca el campo como anulable o dale un valor por defecto

El código fromJson generado convierte exactamente igual que lo harías a mano, así que un campo no anulable con @JsonKey produce el mismo error en tiempo de ejecución cuando faltan los datos. Corrígelo en la declaración del modelo y regenera:

// 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);
}

Un campo anulable hace que el generador emita as String?. Un @JsonKey(defaultValue: ...) hace que sustituya el valor por defecto cuando la clave falta o es null. Ejecuta dart run build_runner build --delete-conflicting-outputs tras cambiar las anotaciones, de lo contrario la vieja conversión generada es lo que se ejecuta.

5. Corrige el desajuste de forma cuando el valor no es realmente null

Si el error nombra un tipo como 'String' pero el payload claramente tiene un valor, el valor tiene la forma equivocada. Un backend que envía "age": "30" (cadena) cuando conviertes as int, o "30" donde esperas un número, desencadena la misma familia de error. Coacciona explícitamente en lugar de convertir:

// 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());

Este no es el caso de Null, pero lleva a la gente a esta página porque la forma del mensaje es idéntica. Cuando el tipo en el mensaje no es 'Null' en el lado izquierdo, mira lo que el servidor realmente envió, no tu manejo de nulos.

Trampas y variantes

El modelo mental para llevarse: este error es null safety negándose a dejar que un null finja ser algo que no es. El valor entró como null, y alguna conversión o asignación aguas abajo prometió que no lo sería. La solución nunca es forzar la conversión con más fuerza; es decidir, en la frontera donde los datos dynamic entran en tu mundo tipado, si ese campo puede estar ausente. Si puede, hazlo anulable y maneja el null. Si no puede, dale un valor por defecto antes de que la conversión se complete. Haz eso en cada búsqueda json[...] y este error deja de aparecer.

Relacionado

Fuentes

Comments

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

< Volver