JSON vers Dart
Générer des classes Dart depuis du JSON avec fromJson et toJson
Entrée JSON
Sortie Dart
Qu'est-ce que la conversion JSON vers Dart ?
La conversion JSON vers Dart prend un objet JSON brut et produit des définitions de classes Dart avec des champs typés, un constructeur nommé, une factory fromJson et une méthode toJson. Dart ne dispose pas de réflexion à l'exécution dans Flutter (dart:mirrors est désactivé), ce qui signifie qu'on ne peut pas désérialiser du JSON en objets typés sans écrire du code de mapping explicite. Chaque réponse d'API REST, document Firebase ou charge utile de configuration nécessite une classe modèle Dart correspondante pour accéder à ses champs avec la sécurité de type.
Une classe modèle Dart typique pour JSON déclare des champs final pour chaque clé, un constructeur avec des paramètres nommés (en utilisant le mot-clé required pour les champs non nullables), un constructeur factory nommé fromJson qui lit depuis une Map de String vers dynamic, et une méthode toJson qui retourne une Map de String vers dynamic. Les objets JSON imbriqués deviennent des classes séparées. Les tableaux deviennent des champs List typés. Les valeurs JSON nullables utilisent la syntaxe null-safety de Dart avec le suffixe ? sur le type.
Écrire ces classes modèles à la main implique de lire chaque clé JSON, de choisir le type Dart, de créer le cast fromJson pour chaque champ (y compris le mapping de listes avec .map().toList()), de construire un littéral de map toJson, et de répéter le tout pour chaque objet imbriqué. Pour un objet JSON avec 12 champs et 2 objets imbriqués, cela représente 3 classes, 6 lignes de factory et des dizaines d'expressions de cast. Un convertisseur produit tout cela en quelques millisecondes depuis un simple collage.
Pourquoi utiliser un convertisseur JSON vers Dart ?
Écrire manuellement des classes modèles Dart depuis du JSON implique de lire les noms de champs, de deviner les types à partir des valeurs d'exemple, d'écrire les casts fromJson avec une gestion correcte des nulls, et de répéter le processus pour les objets imbriqués. Quand la structure de l'API change, chaque mise à jour de champ touche le constructeur, fromJson et toJson. Un convertisseur élimine ce travail répétitif.
Cas d'usage de JSON vers Dart
Correspondance de types JSON vers Dart
Chaque valeur JSON correspond à un type Dart spécifique. Le tableau ci-dessous montre comment le convertisseur traduit chaque type JSON. La colonne Alternative présente les types utilisés dans des scénarios moins courants ou de mapping manuel.
| Type JSON | Exemple | Type Dart | Alternative |
|---|---|---|---|
| string | "hello" | String | String |
| number (integer) | 42 | int | int |
| number (float) | 3.14 | double | double |
| boolean | true | bool | bool |
| null | null | dynamic | Null / dynamic |
| object | {"k": "v"} | NestedClass | Map<String, dynamic> |
| array of strings | ["a", "b"] | List<String> | List<String> |
| array of objects | [{"id": 1}] | List<Item> | List<Item> |
| mixed array | [1, "a"] | List<dynamic> | List<dynamic> |
Approches de sérialisation JSON en Dart
Dart et Flutter proposent plusieurs façons de gérer la sérialisation JSON. Le fromJson/toJson manuel est l'approche la plus simple et ne nécessite pas de génération de code. Pour les projets plus importants, les solutions basées sur build_runner comme json_serializable et freezed génèrent le code répétitif à la compilation, réduisant les erreurs et maintenant les modèles cohérents avec le contrat JSON.
| Approche | Description | Source |
|---|---|---|
| json_serializable | Code-generation-based JSON serialization. Generates .g.dart files at build time via build_runner. Type-safe and compile-time verified. | Official (Google) |
| freezed | Generates immutable data classes with copyWith, fromJson/toJson, equality, and pattern matching support. Built on top of json_serializable. | Community (rrousselGit) |
| built_value | Generates immutable value types with serialization. Enforces immutability patterns. Used in larger codebases with strict data modeling. | |
| dart_mappable | Annotation-based mapper that generates fromJson, toJson, copyWith, and equality. Simpler setup than freezed with similar features. | Community |
| Manual fromJson/toJson | Hand-written factory constructors and toJson methods. No code generation needed. Full control over the mapping logic. | Built-in Dart |
Manuel vs json_serializable vs freezed
Dart propose trois approches courantes pour les classes modèles JSON. Le fromJson/toJson manuel n'a aucune dépendance. json_serializable automatise le code de mapping. freezed ajoute l'immutabilité, copyWith et le pattern matching par-dessus json_serializable.
Exemples de code
Ces exemples montrent comment utiliser les classes Dart générées pour la désérialisation JSON, comment configurer json_serializable avec build_runner, et comment générer des classes Dart par programmation depuis JavaScript et Python.
import 'dart:convert';
class User {
final int id;
final String name;
final String email;
final bool active;
final Address address;
final List<String> tags;
User({
required this.id,
required this.name,
required this.email,
required this.active,
required this.address,
required this.tags,
});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
active: json['active'] as bool,
address: Address.fromJson(json['address'] as Map<String, dynamic>),
tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
'active': active,
'address': address.toJson(),
'tags': tags,
};
}
class Address {
final String street;
final String city;
final String zip;
Address({required this.street, required this.city, required this.zip});
factory Address.fromJson(Map<String, dynamic> json) => Address(
street: json['street'] as String,
city: json['city'] as String,
zip: json['zip'] as String,
);
Map<String, dynamic> toJson() => {'street': street, 'city': city, 'zip': zip};
}
void main() {
final jsonStr = '{"id":1,"name":"Alice","email":"alice@example.com","active":true,"address":{"street":"123 Main","city":"Springfield","zip":"12345"},"tags":["admin","user"]}';
final user = User.fromJson(jsonDecode(jsonStr));
print(user.name); // -> Alice
print(jsonEncode(user.toJson())); // -> round-trip back to JSON
}// pubspec.yaml dependencies:
// json_annotation: ^4.8.0
// dev_dependencies:
// build_runner: ^2.4.0
// json_serializable: ^6.7.0
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // generated by: dart run build_runner build
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
@JsonKey(name: 'is_active')
final bool isActive;
final List<String> tags;
User({
required this.id,
required this.name,
required this.email,
required this.isActive,
required this.tags,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
// Run code generation:
// dart run build_runner build --delete-conflicting-outputsfunction jsonToDart(obj, name = "Root") {
const classes = [];
function infer(val, fieldName) {
if (val === null) return "dynamic";
if (typeof val === "string") return "String";
if (typeof val === "number") return Number.isInteger(val) ? "int" : "double";
if (typeof val === "boolean") return "bool";
if (Array.isArray(val)) {
const first = val.find(v => v !== null);
if (!first) return "List<dynamic>";
return `List<${infer(first, fieldName)}>`;
}
if (typeof val === "object") {
const cls = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
build(val, cls);
return cls;
}
return "dynamic";
}
function build(obj, cls) {
const fields = Object.entries(obj).map(([k, v]) =>
` final ${infer(v, k)} ${k};`
);
classes.push(`class ${cls} {\n${fields.join("\n")}\n}`);
}
build(obj, name);
return classes.join("\n\n");
}
console.log(jsonToDart({ id: 1, name: "Alice", scores: [98, 85] }, "User"));
// class User {
// final int id;
// final String name;
// final List<int> scores;
// }import json
def json_to_dart(obj: dict, class_name: str = "Root") -> str:
classes = []
def infer(val, name):
if val is None:
return "dynamic"
if isinstance(val, bool):
return "bool"
if isinstance(val, int):
return "int"
if isinstance(val, float):
return "double"
if isinstance(val, str):
return "String"
if isinstance(val, list):
if not val:
return "List<dynamic>"
return f"List<{infer(val[0], name)}>"
if isinstance(val, dict):
cls = name[0].upper() + name[1:]
build(val, cls)
return cls
return "dynamic"
def build(obj, cls):
fields = [f" final {infer(v, k)} {k};" for k, v in obj.items()]
classes.append(f"class {cls} {{\n" + "\n".join(fields) + "\n}")
build(obj, class_name)
return "\n\n".join(classes)
data = json.loads('{"id": 1, "name": "Alice", "active": true}')
print(json_to_dart(data, "User"))
# class User {
# final int id;
# final String name;
# final bool active;
# }