JSON إلى Dart
توليد كلاسات Dart من JSON مع fromJson وtoJson
إدخال JSON
مخرجات Dart
ما هو تحويل JSON إلى Dart؟
يأخذ تحويل JSON إلى Dart كائن JSON خامًا وينتج تعريفات كلاسات Dart بحقول محددة الأنواع ومُنشئ بمعاملات مسمّاة ومصنع fromJson وتابع toJson. لا يدعم Flutter الانعكاس في وقت التشغيل (dart:mirrors معطّل)، لذا لا يمكن تحويل JSON إلى كائنات محددة الأنواع دون كتابة كود تعيين صريح. كل رد من API أو مستند Firebase أو حمولة إعداد يحتاج إلى كلاس نموذج Dart مقابل قبل أن تتمكن من الوصول إلى حقوله بطريقة آمنة الأنواع.
يُعرّف كلاس نموذج Dart القياسي لـJSON حقولًا final لكل مفتاح، ومُنشئًا بمعاملات مسمّاة (يستخدم الكلمة المفتاحية required للحقول غير القابلة للإلغاء)، ومُنشئ مصنع باسم fromJson يقرأ من Map<String, dynamic>، وتابع toJson يُعيد Map<String, dynamic>. تصبح كائنات JSON المتداخلة كلاسات منفصلة. تصبح المصفوفات حقول List موصوفة بالأنواع. تستخدم قيم JSON القابلة للإلغاء صيغة سلامة null في Dart مع اللاحقة ? على النوع.
كتابة كلاسات النماذج هذه يدويًا يعني قراءة كل مفتاح JSON وتحديد نوع Dart المناسب وإنشاء تحويل fromJson لكل حقل (بما في ذلك تعيين القوائم باستخدام .map().toList()) وبناء literal لخريطة toJson وتكرار ذلك لكل كائن متداخل. لكائن JSON بـ12 حقلًا وكائنَين متداخلَين، هذا يعني 3 كلاسات و6 أسطر مصنع وعشرات من تعبيرات التحويل. يُنتج المحوّل كل هذا في أجزاء من الثانية من لصقة واحدة.
لماذا تستخدم محوّل JSON إلى Dart؟
كتابة كلاسات نموذج Dart يدويًا من JSON تعني قراءة أسماء الحقول وتخمين الأنواع من القيم النموذجية وكتابة تحويلات fromJson مع معالجة صحيحة للقيم الفارغة وتكرار العملية للكائنات المتداخلة. عند تغيّر شكل الـAPI، كل تحديث للحقل يلمس المُنشئ وfromJson وtoJson. يُزيل المحوّل هذا العمل المتكرر.
حالات استخدام تحويل JSON إلى 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 وfreezes الشيفرة المتكررة وقت التصريف، مما يُقلل الأخطاء ويُبقي النماذج متسقة مع عقد 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 |
يدوي مقابل json_serializable مقابل 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;
# }