Kodokon kodokon.com

Modelar tus datos: clases, JSON e inmutabilidad

Convierte JSON en clases Dart inmutables con fromJson, toJson, copyWith y una igualdad por valor fiable.

9 min · 3 preguntas

Abrir esta lección en Kodokon

A este nivel, el verdadero tema ya no es la sintaxis de las clases, sino la frontera entre el mundo exterior y tu código. Mientras una respuesta de API circule como un Map<String, dynamic>, cada lectura de un campo es una apuesta en tiempo de ejecución. La regla profesional: convierte el JSON en objetos tipados en el momento en que lo recibes y, a partir de ahí, haz circular únicamente esos objetos. Combina esto con la inmutabilidad (campos final, constructor const) y eliminas toda una familia de errores: ya nadie puede modificar un objeto compartido a tus espaldas, y dos instancias const idénticas quedan canonizadas por el compilador.

DART
class User {
  const User({
    required this.id,
    required this.name,
    this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: (json['id'] as num).toInt(),
      name: json['name'] as String,
      email: json['email'] as String?,
    );
  }

  final int id;
  final String name;
  final String? email;
}
Un modelo inmutable con un parseo defensivo del JSON.

Tres decisiones merecen explicarse. Primero, (json['id'] as num).toInt(): muchas API serializan un entero unas veces como 3 y otras como 3.0 según el backend; un cast directo a int se rompe con un double. Después, as String falla de inmediato con un TypeError claro si el campo no existe: eso es intencional. Fallar rápido en el momento del parseo es mejor que un null que se propaga y explota tres pantallas más adelante. Por último, as String? documenta en el tipo que el campo es realmente opcional.

DART
class User {
  const User({required this.id, this.email});

  final int id;
  final String? email;

  Map<String, dynamic> toJson() => {
        'id': id,
        'email': email,
      };

  User copyWith({int? id, String? email}) {
    return User(
      id: id ?? this.id,
      email: email ?? this.email,
    );
  }
}
toJson y copyWith en un modelo reducido a dos campos.

La inmutabilidad exige una igualdad por valor. Por defecto, Dart compara referencias: dos User idénticos campo por campo se consideran "diferentes". Es un problema concreto en cuanto comparas estados: en los tests, en el select de Provider, en una búsqueda dentro de una lista. Redefine == y hashCode juntos, siempre.

DART
class Point {
  const Point(this.x, this.y);

  final int x;
  final int y;

  @override
  bool operator ==(Object other) =>
      other is Point && other.x == x && other.y == y;

  @override
  int get hashCode => Object.hash(x, y);
}
Igualdad por valor: == y hashCode van en pareja.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Por qué escribir (json['id'] as num).toInt() en lugar de json['id'] as int?
    • Porque as int es más lento que as num en tiempo de ejecución.
    • Porque un backend puede serializar un entero como 3.0: un cast directo a int fallaría con un double.
    • Porque json['id'] siempre es una cadena.
  2. ¿Qué produce user.copyWith(email: null) con la implementación clásica de copyWith?
    • El campo email se restablece a null como se solicitó.
    • Se lanza una excepción en tiempo de ejecución.
    • El objeto conserva su email actual: null ?? this.email recae en el valor existente.
    • El código no compila en modo estricto.
  3. ¿Por qué redefinir == y hashCode en un modelo inmutable?
    • Porque Dart compara referencias por defecto: de lo contrario, dos objetos idénticos campo por campo serían "diferentes".
    • Para acelerar la creación de instancias const.
    • Es obligatorio en cuanto un campo se declara final.