Kodokon kodokon.com

Modéliser ses données : classes, JSON et immutabilité

Transformez le JSON en classes Dart immuables avec fromJson, toJson, copyWith et une égalité de valeur fiable.

9 min · 3 questions

Ouvrir cette leçon dans Kodokon

À ce niveau, le vrai sujet n'est plus la syntaxe des classes mais la frontière entre le monde extérieur et votre code. Tant qu'une réponse d'API circule sous forme de Map<String, dynamic>, chaque lecture de champ est un pari au runtime. La règle du métier : convertir le JSON en objets typés dès la réception, puis ne faire circuler que ces objets. Combinez cela avec l'immutabilité - champs final, constructeur const - et vous éliminez toute une famille de bugs : personne ne peut plus modifier un objet partagé dans votre dos, et deux instances const identiques sont canonicalisées par le compilateur.

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 modèle immuable avec parsing défensif du JSON.

Trois choix méritent d'être explicités. D'abord (json['id'] as num).toInt() : beaucoup d'API sérialisent un entier tantôt en 3, tantôt en 3.0 selon le backend ; un cast direct en int plante sur un double. Ensuite, as String échoue immédiatement avec une TypeError claire si le champ manque : c'est voulu. Échouer vite au parsing vaut mieux qu'un null qui se propage et explose trois écrans plus loin. Enfin, as String? documente dans le type que le champ est réellement optionnel.

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 et copyWith sur un modèle réduit à deux champs.

L'immutabilité appelle l'égalité de valeur. Par défaut, Dart compare les références : deux User identiques champ à champ sont considérés « différents ». C'est un problème concret dès que vous comparez des états - dans les tests, dans le select de Provider, dans une recherche de liste. Redéfinissez == et hashCode ensemble, toujours.

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);
}
Égalité de valeur : == et hashCode vont par paire.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Pourquoi écrire (json['id'] as num).toInt() plutôt que json['id'] as int ?
    • Parce que as int est plus lent que as num à l'exécution.
    • Parce qu'un backend peut sérialiser un entier en 3.0 : le cast direct en int échouerait sur un double.
    • Parce que json['id'] est toujours une chaîne de caractères.
  2. Que produit user.copyWith(email: null) avec l'implémentation classique de copyWith ?
    • Le champ email est remis à null comme demandé.
    • Une exception est levée à l'exécution.
    • L'objet garde son email actuel : null ?? this.email retombe sur la valeur existante.
    • Le code ne compile pas en mode strict.
  3. Pourquoi redéfinir == et hashCode sur un modèle immuable ?
    • Parce que Dart compare les références par défaut : deux objets identiques champ à champ seraient sinon « différents ».
    • Pour accélérer la création des instances const.
    • C'est obligatoire dès qu'un champ est déclaré final.