Convierte JSON en clases Dart inmutables con fromJson, toJson, copyWith y una igualdad por valor fiable.
Abrir esta lección en KodokonA 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.
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;
}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.
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,
);
}
}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.
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);
}(json['id'] as num).toInt() en lugar de json['id'] as int?as int es más lento que as num en tiempo de ejecución.3.0: un cast directo a int fallaría con un double.json['id'] siempre es una cadena.user.copyWith(email: null) con la implementación clásica de copyWith?email se restablece a null como se solicitó.email actual: null ?? this.email recae en el valor existente.== y hashCode en un modelo inmutable?const.final.