Turn JSON into immutable Dart classes with fromJson, toJson, copyWith, and reliable value equality.
Open this lesson in KodokonAt this level, the real subject is no longer class syntax but the boundary between the outside world and your code. As long as an API response travels around as a Map<String, dynamic>, every field read is a runtime gamble. The professional rule: convert JSON into typed objects the moment you receive it, then pass only those objects around. Combine this with immutability - final fields, const constructor - and you eliminate a whole family of bugs: nobody can modify a shared object behind your back anymore, and two identical const instances are canonicalized by the compiler.
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;
}Three choices deserve to be spelled out. First, (json['id'] as num).toInt(): many APIs serialize an integer sometimes as 3, sometimes as 3.0 depending on the backend; a direct cast to int crashes on a double. Next, as String fails immediately with a clear TypeError if the field is missing: that's intentional. Failing fast at parse time beats a null that propagates and blows up three screens later. Finally, as String? documents in the type that the field really is optional.
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,
);
}
}Immutability calls for value equality. By default, Dart compares references: two field-for-field identical Users are considered "different." That's a concrete problem the moment you compare states - in tests, in Provider's select, in a list search. Redefine == and hashCode together, always.
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() rather than json['id'] as int?as int is slower than as num at runtime.3.0: a direct cast to int would fail on a double.json['id'] is always a string.user.copyWith(email: null) produce with the classic copyWith implementation?email field is reset to null as requested.email: null ?? this.email falls back to the existing value.== and hashCode on an immutable model?const instances.final.