Kodokon kodokon.com

Daten modellieren: Klassen, JSON und Unveränderlichkeit

Verwandle JSON in unveränderliche Dart-Klassen mit fromJson, toJson, copyWith und zuverlässiger Wertgleichheit.

9 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Auf diesem Niveau geht es nicht mehr um die Syntax von Klassen, sondern um die Grenze zwischen der Außenwelt und deinem Code. Solange eine API-Antwort als Map<String, dynamic> durch die App wandert, ist jeder Feldzugriff ein Glücksspiel zur Laufzeit. Die professionelle Regel: Wandle JSON sofort beim Empfang in typisierte Objekte um und reiche danach nur noch diese Objekte weiter. Kombiniere das mit Unveränderlichkeit - final-Felder, const-Konstruktor - und du eliminierst eine ganze Familie von Bugs: Niemand kann mehr ein geteiltes Objekt hinter deinem Rücken verändern, und zwei identische const-Instanzen werden vom Compiler kanonisiert.

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;
}
Ein unveränderliches Modell mit defensivem JSON-Parsing.

Drei Entscheidungen verdienen eine Erklärung. Erstens (json['id'] as num).toInt(): Viele APIs serialisieren eine Ganzzahl mal als 3, mal als 3.0, je nach Backend; ein direkter Cast auf int stürzt bei einem double ab. Zweitens schlägt as String sofort mit einem klaren TypeError fehl, wenn das Feld fehlt: Das ist Absicht. Ein schnelles Scheitern beim Parsen ist besser als ein null, das sich weiterträgt und drei Bildschirme später explodiert. Und drittens dokumentiert as String? im Typ, dass das Feld wirklich optional ist.

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 und copyWith an einem auf zwei Felder reduzierten Modell.

Unveränderlichkeit verlangt nach Wertgleichheit. Standardmäßig vergleicht Dart Referenzen: Zwei Feld für Feld identische User gelten als "verschieden". Das ist ein konkretes Problem, sobald du Zustände vergleichst - in Tests, in Providers select, bei der Suche in einer Liste. Definiere == und hashCode immer gemeinsam neu.

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);
}
Wertgleichheit: == und hashCode gehören zusammen.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Warum schreibt man (json['id'] as num).toInt() statt json['id'] as int?
    • Weil as int zur Laufzeit langsamer ist als as num.
    • Weil ein Backend eine Ganzzahl als 3.0 serialisieren kann: Ein direkter Cast auf int würde bei einem double fehlschlagen.
    • Weil json['id'] immer eine Zeichenkette ist.
  2. Was ergibt user.copyWith(email: null) mit der klassischen copyWith-Implementierung?
    • Das Feld email wird wie gewünscht auf null zurückgesetzt.
    • Zur Laufzeit wird eine Ausnahme geworfen.
    • Das Objekt behält seine aktuelle email: null ?? this.email fällt auf den bestehenden Wert zurück.
    • Der Code kompiliert im strikten Modus nicht.
  3. Warum sollte man == und hashCode an einem unveränderlichen Modell neu definieren?
    • Weil Dart standardmäßig Referenzen vergleicht: Zwei Feld für Feld identische Objekte wären sonst "verschieden".
    • Um das Erzeugen von const-Instanzen zu beschleunigen.
    • Es ist zwingend nötig, sobald ein Feld als final deklariert ist.