Model Class: fromJson/toJson
Ubah Map JSON menjadi object Dart yang type-safe.
Masalah yang Diselesaikan Model Class
Mengakses data['nama'] secara langsung punya tiga masalah yang makin terasa seiring project membesar:
- Rawan typo. Salah ketik
'nama'menjadi'nmaa'tidak ketahuan sampai aplikasinya crash di runtime. Compiler diam saja karena key-nya berupa string bebas. - Tanpa autocompletion. Editor tidak tahu field apa saja yang tersedia, jadi kamu harus menghafal struktur JSON atau bolak-balik membuka dokumentasi API.
- Logika parsing tersebar. Setiap widget yang butuh data produk menulis ulang casting dan default value sendiri. Saat API berubah, kamu harus berburu ke puluhan file.
Model class menyelesaikan ketiganya: satu class Dart yang type-safe, dengan factory fromJson sebagai satu-satunya pintu masuk parsing, dan toJson untuk mengubah kembali menjadi JSON saat mengirim data ke server.
Contoh Sederhana: Model Produk
class Produk {
final int id;
final String nama;
final int harga;
const Produk({required this.id, required this.nama, required this.harga});
factory Produk.fromJson(Map<String, dynamic> json) {
return Produk(
id: (json['id'] as num).toInt(),
nama: json['nama'] as String? ?? '',
harga: (json['harga'] as num?)?.toInt() ?? 0,
);
}
Map<String, dynamic> toJson() {
return {'id': id, 'nama': nama, 'harga': harga};
}
}Memakainya jauh lebih enak:
final produk = Produk.fromJson(jsonDecode(jsonString));
print(produk.nama); // type-safe, ada autocompletion, typo ketahuan saat compile
// list:
final list = (jsonDecode(jsonList) as List)
.map((e) => Produk.fromJson(e as Map<String, dynamic>))
.toList();Perhatikan (json['id'] as num).toInt(): menangkap sebagai num membuat parsing tahan terhadap API yang kadang mengirim 1 (int) dan kadang 1.0 (double).
Contoh Lebih Nyata: Model Bersarang + copyWith
Data nyata jarang datar. Model bisa berisi model lain, dan copyWith membuat object immutable mudah diubah sebagian:
class Pesanan {
final String id;
final List<Produk> items;
final DateTime dibuatPada;
const Pesanan({
required this.id,
required this.items,
required this.dibuatPada,
});
factory Pesanan.fromJson(Map<String, dynamic> json) {
final itemsJson = json['items'] as List? ?? [];
return Pesanan(
id: json['id'] as String? ?? '',
items: itemsJson
.map((e) => Produk.fromJson(e as Map<String, dynamic>))
.toList(),
dibuatPada: DateTime.tryParse(json['dibuat_pada'] as String? ?? '') ??
DateTime.now(),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'items': items.map((e) => e.toJson()).toList(),
'dibuat_pada': dibuatPada.toIso8601String(),
};
int get total => items.fold(0, (sum, p) => sum + p.harga);
Pesanan copyWith({String? id, List<Produk>? items, DateTime? dibuatPada}) {
return Pesanan(
id: id ?? this.id,
items: items ?? this.items,
dibuatPada: dibuatPada ?? this.dibuatPada,
);
}
}copyWith penting karena field-nya final (immutable): untuk mengubah satu field, kamu membuat salinan baru alih-alih memutasi object lama. Ini pola standar di state management modern (Riverpod, Bloc) karena perubahan state jadi terprediksi dan mudah di-track.
Edge Cases yang Sering Menjebak
Key JSON beda nama dengan field Dart. API memakai snake_case (dibuat_pada) sedangkan Dart memakai camelCase (dibuatPada). Mapping manual di fromJson/toJson seperti contoh di atas menyelesaikannya, tapi untuk puluhan field, code generator jauh lebih praktis.
List null atau berisi null. (json['items'] as List? ?? []) menangani key yang hilang, tapi item di dalamnya juga bisa null dari API nakal. Untuk extra aman: .where((e) => e != null) sebelum mapping.
DateTime gagal parse. DateTime.parse melempar error kalau formatnya salah. Pakai DateTime.tryParse yang mengembalikan null, lalu beri fallback.
toJson untuk dikirim ke server. Pastikan formatnya sesuai ekspektasi backend: kirim dibuatPada.toIso8601String() bukan object DateTime mentah (tidak bisa di-encode), dan pastikan harga tetap int kalau backend menolak double.
Equality. Dua object Produk dengan isi sama dianggap beda oleh == default (perbandingan identitas). Kalau butuh perbandingan nilai (misalnya untuk cek duplikat di keranjang), override == dan hashCode, atau pakai package equatable.
Kapan TIDAK Perlu Model Class
- JSON super sederhana dan sekali pakai (misalnya response
{"ok": true}):Mapmentah lebih cepat ditulis. - Prototyping cepat: sah-sah saja mulai dari
Map, lalu refactor ke model saat strukturnya stabil. Jangan biarkan map mentah bertahan sampai production kalau datanya dipakai di banyak tempat.
Best Practices dan Code Generator
Untuk project besar, menulis fromJson/toJson manual untuk 30 model itu melelahkan dan rawan inkonsisten. Pakai code generator:
json_serializable: cukup tambah annotation@JsonSerializable(), jalankanbuild_runner, dan semua method parsing dibuat otomatis, termasuk mappingsnake_caseviafieldRename.freezed: selevel di atasnya, otomatis dapatcopyWith,==/hashCode, dan union types, cocok untuk state management.
Aturan mainnya: model yang ditulis manual wajib punya field final, constructor const kalau memungkinkan, dan satu factory fromJson sebagai satu-satunya tempat parsing. Jangan pernah parsing JSON produk di luar Produk.fromJson, itulah gunanya sentralisasi.
Catatan teknis: Untuk project besar, pakai code generator seperti
json_serializableataufreezedagarfromJson/toJsondibuat otomatis dan konsisten. Tapi pahami dulu cara manualnya (seperti di modul ini) sebelum memakai generator: saat generated code bermasalah, kamu harus bisa membaca dan memperbaikinya sendiri.
Tantangan
Model Pengguna
Buat class Pengguna dengan field id (int), nama (String), email (String), plus factory fromJson dan method toJson yang aman null.