الشبكات وتكامل REST API

التسلسل التلقائي لـ JSON باستخدام json_serializable

15 دقيقة الدرس 5 من 13

التسلسل التلقائي لـ JSON باستخدام json_serializable

كتابة دوال fromJson وtoJson يدوياً أمر ممل وعرضة للأخطاء. تحل حزمة json_serializable هذه المشكلة بتوليد تلك الشيفرة المكررة تلقائياً. تُضيف تعليقاً توضيحياً (annotation) لفئة نموذج Dart الخاصة بك، تُشغّل مُولِّد الشيفرة build_runner، وتنتج الحزمة ملف .g.dart يحتوي على منطق التسلسل الكامل. والنتيجة معالجة JSON آمنة النوع ولا تحتاج صيانة.

ملاحظة: json_serializable هي اعتمادية تطوير فقط — لا تحتاجها إلا خلال التطوير لتوليد الشيفرة. ملفات .g.dart المُولَّدة تُضاف إلى نظام التحكم بالمصدر وتُشحن مع التطبيق. لا يوجد أدوات توليد شيفرة في وقت التشغيل.

إعداد الاعتماديات

أضف الحزم المطلوبة إلى pubspec.yaml. توفر حزمة json_annotation التعليقات التوضيحية التي تستخدمها في نموذجك. تقوم json_serializable وbuild_runner بتوليد الشيفرة خلال وقت التطوير.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^4.9.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.0
  json_serializable: ^6.8.0

شغّل flutter pub get لتثبيت الحزم قبل إضافة التعليقات التوضيحية لنماذجك.

إضافة التعليقات التوضيحية لفئة النموذج

لجعل فئة قابلة للتسلسل، تحتاج ثلاثة أشياء:

  • استيراد package:json_annotation/json_annotation.dart
  • إضافة توجيه part يشير إلى الملف المُولَّد
  • تزيين الفئة بـ @JsonSerializable()

user.dart — نموذج مُزيَّن بالتعليقات

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart'; // سيوضع الملف المُولَّد هنا

@JsonSerializable()
class User {
  final int id;
  final String name;
  final String email;

  @JsonKey(name: 'avatar_url') // ربط مفتاح JSON بـ snake_case بحقل camelCase
  final String avatarUrl;

  @JsonKey(name: 'created_at')
  final DateTime createdAt;

  const User({
    required this.id,
    required this.name,
    required this.email,
    required this.avatarUrl,
    required this.createdAt,
  });

  // المصنع والدالة يُفوِّضان للشيفرة المُولَّدة:
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

تشغيل مُولِّد الشيفرة

بعد تزيين نموذجك، شغّل أحد الأوامر التالية من جذر المشروع:

  • بناء لمرة واحدة: dart run build_runner build --delete-conflicting-outputs
  • وضع المراقبة: dart run build_runner watch --delete-conflicting-outputs

يُنشئ المُولِّد user.g.dart بجانب user.dart. لا يجب أبداً تعديل هذا الملف يدوياً — سيُستبدل عند البناء التالي.

نصيحة: استخدم وضع المراقبة خلال التطوير النشط. يراقب ملفات مصدرك ويُعيد توليد ملفات .g.dart فوراً عند حفظ أي تغيير لفئة مُزيَّنة، لذا لن تحتاج للتذكر بإعادة تشغيل البناء يدوياً.

محتوى الملف المُولَّد

بعد تشغيل المُولِّد، سيحتوي user.g.dart على دالتين على مستوى أعلى تُفوِّض إليهما الفئة النموذجية:

user.g.dart — المخرج المُولَّد (لا تُعدِّله)

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'user.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

User _$UserFromJson(Map<String, dynamic> json) => User(
      id: (json['id'] as num).toInt(),
      name: json['name'] as String,
      email: json['email'] as String,
      avatarUrl: json['avatar_url'] as String,
      createdAt: DateTime.parse(json['created_at'] as String),
    );

Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
      'id': instance.id,
      'name': instance.name,
      'email': instance.email,
      'avatar_url': instance.avatarUrl,
      'created_at': instance.createdAt.toIso8601String(),
    };

خيارات @JsonSerializable المفيدة

تقبل حاشية @JsonSerializable معاملات اختيارية لتخصيص السلوك:

  • fieldRename: FieldRename.snake — يُعيِّن تلقائياً جميع حقول camelCase لمفاتيح JSON بـ snake_case دون الحاجة لـ @JsonKey على كل حقل.
  • explicitToJson: true — مطلوب عندما يكون الحقل نفسه كائناً قابلاً للتسلسل؛ وإلا لن تُحوَّل الكائنات المتداخلة إلى خرائط.
  • includeIfNull: false — يُغفل الحقول ذات قيمة null من المخرج المُسلسَل.
  • createToJson: false — يتخطى توليد toJson عندما تحتاج التسلسل من JSON فقط (مثلاً استجابات API للقراءة فقط).

كائنات متداخلة مع explicitToJson

@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
class Post {
  final int id;
  final String title;
  final User author; // كائن متداخل قابل للتسلسل

  const Post({required this.id, required this.title, required this.author});

  factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
  Map<String, dynamic> toJson() => _$PostToJson(this);
}
تحذير: إذا نسيت explicitToJson: true عندما يحتوي النموذج على حقول متداخلة قابلة للتسلسل، سيتضمن toJson() الكائن المتداخل كمثيل Dart بدلاً من Map. يُسبب هذا عادةً خطأ في وقت التشغيل أو مخرج JSON غير صحيح عند الترميز.

التعامل مع القوائم والحقول القابلة للإهمال

يتعامل المُولِّد مع List<T> وMap<K, V> والأنواع القابلة للإهمال (T?) دون تهيئة إضافية. صرِّح عن الأنواع بدقة وستتولى الشيفرة المُولَّدة التحويل والتحقق من القيمة الخالية بشكل صحيح.

نموذج مع قائمة وحقول قابلة للإهمال

@JsonSerializable(explicitToJson: true)
class ApiResponse {
  final bool success;
  final String? message;       // قابل للإهمال — قد يغيب في JSON
  final List<User> data;       // قائمة من الكائنات المتداخلة

  const ApiResponse({
    required this.success,
    this.message,
    required this.data,
  });

  factory ApiResponse.fromJson(Map<String, dynamic> json) =>
      _$ApiResponseFromJson(json);
  Map<String, dynamic> toJson() => _$ApiResponseToJson(this);
}

الخلاصة

تُبقي سيرة عمل json_serializable فئات نموذجك نظيفة وتصريحية. تصف شكل بياناتك مرة واحدة باستخدام الحواشي، تُشغِّل المُولِّد، وتحصل على شيفرة تسلسل موثوقة مجاناً. النقاط الرئيسية التي يجب تذكرها:

  • أضف json_annotation كاعتمادية تشغيل، وbuild_runner وjson_serializable كاعتماديات تطوير.
  • زيِّن الفئة بـ @JsonSerializable() وأضف توجيه part.
  • شغّل dart run build_runner build --delete-conflicting-outputs لتوليد ملف .g.dart.
  • استخدم @JsonKey(name: ...) لتعيين المفاتيح المخصصة وexplicitToJson: true للكائنات المتداخلة.
  • أضف ملفات .g.dart المُولَّدة لنظام التحكم بالمصدر — فهي جزء من التطبيق المُجمَّع.