JSON to Dart
Генерація класів Dart з JSON з методами fromJson та toJson
Введення JSON
Виведення Dart
Що таке конвертація JSON у Dart?
Конвертація JSON у Dart бере сирий JSON-об'єкт і генерує визначення класів Dart з типізованими полями, іменованим конструктором, фабрикою fromJson та методом toJson. Dart не підтримує рефлексію під час виконання у Flutter (dart:mirrors вимкнено), тому неможливо десеріалізувати JSON у типізовані об'єкти без явного коду відображення. Кожна відповідь REST API, документ Firebase або конфігураційний пейлоад потребує відповідного класу моделі Dart перед тим, як ви зможете звертатися до його полів з безпекою типів.
Типовий клас моделі Dart для JSON оголошує фінальні поля для кожного ключа, конструктор з іменованими параметрами (із ключовим словом required для non-nullable полів), фабричний конструктор fromJson, що читає з Map<String, dynamic>, та метод toJson, що повертає Map<String, dynamic>. Вкладені JSON-об'єкти стають окремими класами. Масиви перетворюються на типізовані поля List. Nullable JSON-значення використовують синтаксис null-safety Dart з суфіксом ? у типі.
Написання таких класів моделей вручну означає читання кожного JSON-ключа, визначення типу Dart, створення приведення типів у fromJson для кожного поля (включно з відображенням списків через .map().toList()), формування літерала map у toJson та повторення цього для кожного вкладеного об'єкта. Для JSON-об'єкта з 12 полями та 2 вкладеними об'єктами це означає 3 класи, 6 рядків фабрики та десятки виразів приведення типів. Конвертер генерує все це за мілісекунди з одного вставленого фрагмента.
Навіщо використовувати конвертер JSON to Dart?
Ручне написання класів моделей Dart з JSON передбачає читання назв полів, визначення типів за зразками значень, написання приведень типів у fromJson з коректною обробкою null та повторення цього для вкладених об'єктів. Коли форма API змінюється, кожне оновлення поля стосується конструктора, fromJson і toJson. Конвертер усуває цю рутинну роботу.
Сценарії використання JSON to Dart
Таблиця відповідності типів JSON та Dart
Кожне JSON-значення відображається на конкретний тип Dart. Таблиця нижче показує, як конвертер перекладає кожен JSON-тип. Стовпець «Альтернатива» показує типи, що використовуються в менш поширених або ручних сценаріях відображення.
| Тип JSON | Приклад | Тип Dart | Альтернатива |
|---|---|---|---|
| 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> |
Підходи до серіалізації JSON у Dart
Dart і Flutter пропонують кілька способів роботи з серіалізацією JSON. Ручний fromJson/toJson — найпростіший підхід, що не потребує генерації коду. У великих проектах рішення на основі build_runner, як-от json_serializable та freezed, генерують шаблонний код під час компіляції, зменшуючи кількість помилок і забезпечуючи узгодженість моделей з JSON-контрактом.
| Підхід | Опис | Джерело |
|---|---|---|
| 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 |
Manual vs json_serializable vs freezed
У Dart є три поширені підходи для класів JSON-моделей. Ручний fromJson/toJson не має залежностей. json_serializable автоматизує код відображення. freezed додає незмінність, copyWith та зіставлення з образцем поверх json_serializable.
Приклади коду
Ці приклади показують, як використовувати згенеровані класи Dart для десеріалізації JSON, як налаштувати json_serializable з build_runner та як програмно генерувати класи Dart з JavaScript і 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;
# }