JSON in Dart
Genera classi Dart da JSON con fromJson e toJson
Input JSON
Output Dart
Cos'è la conversione da JSON a Dart?
La conversione da JSON a Dart prende un oggetto JSON grezzo e produce definizioni di classi Dart con campi tipizzati, un costruttore con parametri nominati, una factory fromJson e un metodo toJson. Dart non dispone di reflection a runtime in Flutter (dart:mirrors è disabilitato), quindi non è possibile deserializzare JSON in oggetti tipizzati senza scrivere codice di mappatura esplicito. Ogni risposta di API REST, documento Firebase o payload di configurazione richiede una classe modello Dart corrispondente prima di poter accedere ai suoi campi con la sicurezza dei tipi.
Una tipica classe modello Dart per JSON dichiara campi final per ogni chiave, un costruttore con parametri nominati (usando la parola chiave required per i campi non nullable), una factory constructor chiamata fromJson che legge da una Map di String a dynamic, e un metodo toJson che restituisce una Map di String a dynamic. Gli oggetti JSON annidati diventano classi separate. Gli array diventano campi List tipizzati. I valori JSON nullable usano la sintassi null-safety di Dart con il suffisso ? sul tipo.
Scrivere queste classi modello a mano significa leggere ogni chiave JSON, decidere il tipo Dart, creare il cast fromJson per ogni campo (inclusa la mappatura delle liste con .map().toList()), costruire il map literal toJson e ripetere il processo per ogni oggetto annidato. Per un oggetto JSON con 12 campi e 2 oggetti annidati, ciò significa 3 classi, 6 righe di factory e decine di espressioni di cast. Un convertitore produce tutto questo in pochi millisecondi da un singolo incolla.
Perché usare un convertitore da JSON a Dart?
Scrivere manualmente classi modello Dart da JSON implica leggere i nomi dei campi, dedurre i tipi dai valori di esempio, scrivere i cast fromJson con la corretta gestione dei null e ripetere il processo per gli oggetti annidati. Quando la struttura dell'API cambia, ogni aggiornamento di campo tocca il costruttore, fromJson e toJson. Un convertitore elimina questo lavoro ripetitivo.
Casi d'uso di JSON in Dart
Mappatura dei tipi da JSON a Dart
Ogni valore JSON si mappa a un tipo Dart specifico. La tabella seguente mostra come il convertitore traduce ogni tipo JSON. La colonna Alternativa mostra i tipi usati in scenari di mappatura meno comuni o manuali.
| Tipo JSON | Esempio | Tipo Dart | Alternativa |
|---|---|---|---|
| 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> |
Approcci alla serializzazione JSON in Dart
Dart e Flutter offrono diversi modi per gestire la serializzazione JSON. Il metodo manuale fromJson/toJson è l'approccio più semplice e non richiede generazione di codice. Per progetti di maggiori dimensioni, le soluzioni basate su build_runner come json_serializable e freezed generano il codice ripetitivo in fase di compilazione, riducendo gli errori e mantenendo i modelli coerenti con il contratto JSON.
| Approccio | Descrizione | Sorgente |
|---|---|---|
| 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 |
Manuale vs json_serializable vs freezed
Dart offre tre approcci comuni per le classi modello JSON. Il metodo manuale fromJson/toJson non ha dipendenze. json_serializable automatizza il codice di mappatura. freezed aggiunge immutabilità, copyWith e pattern matching sopra json_serializable.
Esempi di codice
Questi esempi mostrano come usare le classi Dart generate per la deserializzazione JSON, come configurare json_serializable con build_runner e come generare classi Dart programmaticamente da JavaScript e 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;
# }