JSON #
JSON adalah bahasa universal pertukaran data — hampir semua API modern menggunakannya. Di Dart, dart:convert menyediakan jsonEncode dan jsonDecode sebagai fondasi, tapi penggunaan yang benar membutuhkan lebih dari sekadar dua fungsi itu. Artikel ini membahas dari decoding yang type-safe, pola fromJson/toJson untuk model kelas, penanganan nested object dan nullable field, hingga json_serializable untuk code generation yang menghilangkan boilerplate serialisasi secara otomatis.
Dasar: jsonEncode dan jsonDecode
#
import 'dart:convert';
// jsonEncode — Dart object → JSON string
// Mendukung: String, int, double, bool, null, List, Map<String, dynamic>
final map = {'nama': 'Budi', 'umur': 25, 'aktif': true};
final jsonString = jsonEncode(map);
print(jsonString); // '{"nama":"Budi","umur":25,"aktif":true}'
// Dengan indentasi — untuk debugging/logging
final rapi = JsonEncoder.withIndent(' ').convert(map);
print(rapi);
// {
// "nama": "Budi",
// "umur": 25,
// "aktif": true
// }
// jsonDecode — JSON string → Dart object
// Mengembalikan dynamic — harus di-cast secara eksplisit
final decoded = jsonDecode(jsonString);
print(decoded.runtimeType); // _Map<String, dynamic>
final nama = decoded['nama'] as String;
final umur = decoded['umur'] as int;
Tipe yang Didukung JSON #
JSON → Dart
null → null
true/false → bool
angka tanpa desimal → int
angka dengan desimal → double
"string" → String
[...] → List<dynamic>
{...} → Map<String, dynamic>
// ANTI-PATTERN: mengakses nilai tanpa cast eksplisit
final data = jsonDecode(response.body);
String nama = data['nama']; // ✗ dynamic tidak bisa langsung ke String
int umur = data['umur']; // ✗ sama
print(data['daftar'].length); // ✗ dynamic — bisa crash saat runtime
// BENAR: cast eksplisit yang aman
final data = jsonDecode(response.body) as Map<String, dynamic>;
final nama = data['nama'] as String;
final umur = data['umur'] as int;
final daftar = data['daftar'] as List<dynamic>;
Penanganan Error Parsing #
jsonDecode melempar FormatException untuk JSON yang tidak valid. Selalu tangkap secara spesifik:
import 'dart:convert';
T? parseJson<T>(String input, T Function(dynamic) parser) {
try {
final decoded = jsonDecode(input);
return parser(decoded);
} on FormatException catch (e) {
print('JSON tidak valid: ${e.message}');
print('Offset: ${e.offset}');
return null;
} on TypeError catch (e) {
print('Tipe tidak sesuai: $e');
return null;
}
}
// Penggunaan
final pengguna = parseJson(
responseBody,
(json) => Pengguna.fromJson(json as Map<String, dynamic>),
);
Model Kelas dengan fromJson dan toJson
#
Pola ini adalah standar dalam Dart — setiap model data memiliki factory constructor fromJson dan method toJson:
import 'dart:convert';
class Pengguna {
final String id;
final String nama;
final String email;
final int umur;
final bool aktif;
const Pengguna({
required this.id,
required this.nama,
required this.email,
required this.umur,
required this.aktif,
});
factory Pengguna.fromJson(Map<String, dynamic> json) {
return Pengguna(
id: json['id'] as String,
nama: json['nama'] as String,
email: json['email'] as String,
umur: json['umur'] as int,
aktif: json['aktif'] as bool? ?? true, // default jika field tidak ada
);
}
Map<String, dynamic> toJson() => {
'id': id,
'nama': nama,
'email': email,
'umur': umur,
'aktif': aktif,
};
// Helper: string JSON → Pengguna
static Pengguna fromJsonString(String source) {
return Pengguna.fromJson(
jsonDecode(source) as Map<String, dynamic>,
);
}
// Helper: Pengguna → string JSON
String toJsonString() => jsonEncode(toJson());
@override
String toString() => 'Pengguna($id: $nama)';
}
Nested Object #
Model yang memiliki field berupa objek lain:
class Alamat {
final String jalan;
final String kota;
final String kodePos;
const Alamat({required this.jalan, required this.kota, required this.kodePos});
factory Alamat.fromJson(Map<String, dynamic> json) => Alamat(
jalan: json['jalan'] as String,
kota: json['kota'] as String,
kodePos: json['kode_pos'] as String,
);
Map<String, dynamic> toJson() => {
'jalan': jalan,
'kota': kota,
'kode_pos': kodePos,
};
}
class PenggunaDenganAlamat {
final String nama;
final Alamat alamat; // nested object — bukan null
final Alamat? alamatPengiriman; // nested nullable — boleh null
const PenggunaDenganAlamat({
required this.nama,
required this.alamat,
this.alamatPengiriman,
});
factory PenggunaDenganAlamat.fromJson(Map<String, dynamic> json) {
return PenggunaDenganAlamat(
nama: json['nama'] as String,
// Nested object wajib
alamat: Alamat.fromJson(json['alamat'] as Map<String, dynamic>),
// Nested object nullable — cek null sebelum parse
alamatPengiriman: json['alamat_pengiriman'] == null
? null
: Alamat.fromJson(json['alamat_pengiriman'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() => {
'nama': nama,
'alamat': alamat.toJson(),
'alamat_pengiriman': alamatPengiriman?.toJson(),
};
}
List of Objects #
class Produk {
final String id;
final String nama;
final double harga;
final List<String> tag;
const Produk({required this.id, required this.nama,
required this.harga, required this.tag});
factory Produk.fromJson(Map<String, dynamic> json) {
return Produk(
id: json['id'] as String,
nama: json['nama'] as String,
harga: (json['harga'] as num).toDouble(), // num menangani int dan double
// List of String dari JSON array
tag: (json['tag'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
}
Map<String, dynamic> toJson() => {
'id': id,
'nama': nama,
'harga': harga,
'tag': tag,
};
}
// Parse list of objects dari JSON array
List<Produk> parseProdukList(String jsonString) {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((json) => Produk.fromJson(json as Map<String, dynamic>))
.toList();
}
// Encode list of objects ke JSON string
String encodeProdukList(List<Produk> produk) {
return jsonEncode(produk.map((p) => p.toJson()).toList());
}
Menangani Tipe yang Tidak Konsisten dari API #
API di dunia nyata sering tidak konsisten — field yang kadang integer, kadang string:
factory Produk.fromJson(Map<String, dynamic> json) {
// Harga kadang int, kadang double, kadang string
final hargaRaw = json['harga'];
final double harga;
if (hargaRaw is num) {
harga = hargaRaw.toDouble();
} else if (hargaRaw is String) {
harga = double.tryParse(hargaRaw) ?? 0.0;
} else {
harga = 0.0;
}
// ID kadang int, kadang string
final id = json['id'].toString(); // toString() aman untuk keduanya
// Boolean kadang 0/1, kadang true/false
final aktifRaw = json['aktif'];
final aktif = aktifRaw == true || aktifRaw == 1 || aktifRaw == '1';
return Produk(id: id, harga: harga, aktif: aktif, /* ... */);
}
json_serializable — Code Generation
#
Untuk model yang banyak, menulis fromJson dan toJson secara manual sangat tedious dan rentan typo. json_serializable menghasilkan kode ini secara otomatis:
dart pub add json_annotation
dart pub add dev:json_serializable dev:build_runner
// lib/src/model/produk.dart
import 'package:json_annotation/json_annotation.dart';
// Bagian yang di-generate (nama file: produk.g.dart)
part 'produk.g.dart';
@JsonSerializable() // anotasi untuk code generation
class Produk {
final String id;
final String nama;
@JsonKey(name: 'harga_jual') // nama field JSON berbeda dari field Dart
final double harga;
@JsonKey(defaultValue: true) // nilai default jika field tidak ada di JSON
final bool aktif;
@JsonKey(name: 'tanggal_dibuat', fromJson: _dateFromJson, toJson: _dateToJson)
final DateTime tanggalDibuat;
@JsonKey(includeIfNull: false) // jangan include di JSON jika null
final String? catatan;
const Produk({
required this.id,
required this.nama,
required this.harga,
required this.aktif,
required this.tanggalDibuat,
this.catatan,
});
// Generated methods — jangan tulis manual
factory Produk.fromJson(Map<String, dynamic> json) =>
_$ProdukFromJson(json);
Map<String, dynamic> toJson() => _$ProdukToJson(this);
// Custom converter untuk DateTime
static DateTime _dateFromJson(String date) => DateTime.parse(date);
static String _dateToJson(DateTime date) => date.toIso8601String();
}
# Generate file *.g.dart
dart run build_runner build
# Watch mode — auto-regenerate saat file berubah
dart run build_runner watch --delete-conflicting-outputs
File produk.g.dart yang di-generate berisi implementasi _$ProdukFromJson dan _$ProdukToJson yang lengkap dan type-safe.
Konfigurasi json_serializable
#
// Konfigurasi global di build.yaml
// build.yaml (di root project)
targets:
$default:
builders:
json_serializable:
options:
# Gunakan field name sebagai JSON key secara default
field_rename: snake_case # namaField → nama_field
# Jangan include null di output JSON secara default
include_if_null: false
# Gunakan explicit_to_json untuk nested objects
explicit_to_json: true
// Dengan konfigurasi snake_case global, tidak perlu @JsonKey untuk setiap field
@JsonSerializable(fieldRename: FieldRename.snake)
class PenggunaModel {
final String namaLengkap; // → 'nama_lengkap' di JSON
final String emailUtama; // → 'email_utama' di JSON
final int nomorTelepon; // → 'nomor_telepon' di JSON
// ...
factory PenggunaModel.fromJson(Map<String, dynamic> json) =>
_$PenggunaModelFromJson(json);
Map<String, dynamic> toJson() => _$PenggunaModelToJson(this);
}
Streaming JSON Besar #
Untuk file JSON yang sangat besar (ratusan MB), parsing sekaligus akan menyebabkan OOM. Gunakan streaming:
import 'dart:io';
import 'dart:convert';
// Stream parsing — proses satu baris JSON per baris (NDJSON/JSON Lines format)
Future<void> prosesJsonLines(String path) async {
final file = File(path);
int diproses = 0;
await file
.openRead()
.transform(utf8.decoder)
.transform(const LineSplitter())
.forEach((baris) {
if (baris.trim().isEmpty) return;
try {
final json = jsonDecode(baris) as Map<String, dynamic>;
prosesSatuRecord(json);
diproses++;
} on FormatException catch (e) {
print('Baris $diproses tidak valid: $e');
}
});
print('Selesai memproses $diproses record');
}
// JsonDecoder sebagai stream transformer
Stream<dynamic> jsonStream(Stream<String> input) {
return input.map((chunk) => jsonDecode(chunk));
}
Encode dengan Custom Encoder #
Untuk tipe yang tidak didukung JSON secara native (seperti DateTime, Enum), buat custom encoder:
import 'dart:convert';
// Custom encoder — tangani tipe non-standar
String encodeWithCustomTypes(dynamic object) {
return jsonEncode(object, toEncodable: (value) {
if (value is DateTime) {
return value.toUtc().toIso8601String();
}
if (value is Enum) {
return value.name;
}
if (value is Uri) {
return value.toString();
}
throw UnsupportedError('Tipe tidak didukung: ${value.runtimeType}');
});
}
// Penggunaan
final data = {
'dibuat': DateTime.now(), // DateTime → string ISO 8601
'status': StatusOrder.aktif, // Enum → string name
'url': Uri.parse('https://dart.dev'), // Uri → string
};
print(encodeWithCustomTypes(data));
// '{"dibuat":"2024-11-15T07:30:00.000Z","status":"aktif","url":"https://dart.dev"}'
Anti-Pattern JSON #
Cast Tanpa Validasi #
// ANTI-PATTERN: chain cast panjang tanpa validasi
final json = jsonDecode(responseBody);
final nama = json['pengguna']['profil']['nama']; // ✗ bisa NullPointerError di setiap level
// BENAR: validasi bertahap dengan penanganan null
final json = jsonDecode(responseBody) as Map<String, dynamic>?;
if (json == null) return null;
final pengguna = json['pengguna'] as Map<String, dynamic>?;
if (pengguna == null) return null;
final profil = pengguna['profil'] as Map<String, dynamic>?;
final nama = profil?['nama'] as String?;
Menggunakan Map<String, dynamic> sebagai Model Permanen
#
// ANTI-PATTERN: simpan dan teruskan Map<String, dynamic> di seluruh codebase
Future<Map<String, dynamic>> ambilPengguna(String id) async {
final response = await http.get(Uri.parse('/api/pengguna/$id'));
return jsonDecode(response.body) as Map<String, dynamic>; // ✗
}
// Di tempat lain:
final data = await ambilPengguna('U001');
print(data['Nama']); // ✗ typo 'Nama' vs 'nama' — tidak ada error saat compile!
// BENAR: parse segera ke model yang type-safe
Future<Pengguna> ambilPengguna(String id) async {
final response = await http.get(Uri.parse('/api/pengguna/$id'));
final json = jsonDecode(response.body) as Map<String, dynamic>;
return Pengguna.fromJson(json); // parse sekali, type-safe selamanya
}
final pengguna = await ambilPengguna('U001');
print(pengguna.nama); // ✓ compiler akan tangkap typo
Lupa Konversi num untuk Angka
#
// ANTI-PATTERN: asumsi harga selalu double
factory Produk.fromJson(Map<String, dynamic> json) {
return Produk(harga: json['harga'] as double); // ✗ jika API kirim 150000 (int), crash!
}
// BENAR: gunakan num.toDouble() — num adalah supertype dari int dan double
factory Produk.fromJson(Map<String, dynamic> json) {
return Produk(harga: (json['harga'] as num).toDouble()); // ✓ aman untuk keduanya
}
Ringkasan #
jsonDecodemengembalikandynamic— selalu cast ke tipe yang diharapkan (as Map<String, dynamic>,as List<dynamic>) sebelum mengakses field.- Pola
fromJson/toJsonadalah standar Dart untuk serialisasi model — parse segera setelah terima data jaringan, jangan teruskanMap<String, dynamic>ke seluruh codebase.(json['harga'] as num).toDouble()untuk field angka —numadalah supertype dariintdandouble, menghindari crash ketika API mengirim integer untuk field yang diharapkan double.- Nested object nullable — cek null dulu sebelum memanggil
fromJson:json['alamat'] == null ? null : Alamat.fromJson(json['alamat']).json_serializableuntuk proyek besar — anotasi@JsonSerializable()dandart run build_runner buildmenghasilkanfromJson/toJsonyang type-safe tanpa boilerplate manual.@JsonKey(name: 'snake_case')untuk memetakan nama field JSON yang berbeda dari nama field Dart, atau gunakanfieldRename: FieldRename.snakesecara global.- Custom encoder dengan
toEncodableuntuk tipe non-standar sepertiDateTime,Enum, danUriyang tidak didukung JSON secara native.- Streaming untuk JSON besar — NDJSON (satu JSON per baris) bisa diproses dengan
LineSplittertanpa memuat seluruh file ke memori.FormatExceptionadalah exception spesifik darijsonDecodeuntuk JSON tidak valid — tangkap secara spesifik, bukancatch (e)generik.JsonEncoder.withIndent(' ')untuk format JSON yang mudah dibaca manusia saat debugging — jangan gunakan di production response untuk menghemat bandwidth.