JSON & Serialisasi #
Hampir semua data yang diterima dari API berbentuk JSON. Konversi antara JSON dan Dart object — yang disebut serialisasi dan deserialisasi — adalah pekerjaan yang berulang dan rawan kesalahan jika dilakukan secara manual. Flutter menyediakan beberapa pendekatan, dari parsing manual yang sederhana hingga code generation yang aman dan otomatis.
Parsing Manual — dart:convert #
Pendekatan paling dasar: gunakan jsonDecode() dari package dart:convert bawaan Dart, lalu petakan hasilnya ke class Dart secara manual.
import 'dart:convert';
class Produk {
final String id;
final String nama;
final double harga;
final bool tersedia;
final List<String> kategori;
const Produk({
required this.id,
required this.nama,
required this.harga,
required this.tersedia,
required this.kategori,
});
// Deserialisasi: JSON → Produk
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(),
tersedia: json['tersedia'] as bool,
kategori: (json['kategori'] as List<dynamic>)
.map((e) => e as String)
.toList(),
);
}
// Serialisasi: Produk → JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'nama': nama,
'harga': harga,
'tersedia': tersedia,
'kategori': kategori,
};
}
}
// Parsing satu objek
final Map<String, dynamic> json = jsonDecode(responseBody);
final produk = Produk.fromJson(json);
// Parsing list
final List<dynamic> jsonList = jsonDecode(responseBody);
final produkList = jsonList.map((j) => Produk.fromJson(j)).toList();
// Encode ke JSON string
final jsonString = jsonEncode(produk.toJson());
Keterbatasan Parsing Manual #
✗ Repetitif -- harus tulis fromJson/toJson untuk setiap class
✗ Rawan typo -- 'nama' vs 'name', tidak ada compile-time check
✗ Tidak ada == override -- dua objek dengan nilai sama dianggap berbeda
✗ Tidak ada copyWith -- harus tulis sendiri
✗ Tidak ada toString yang informatif untuk debugging
✗ Skala buruk -- 20 class = 20x kode boilerplate yang sama
json_serializable — Code Generation #
json_serializable menghasilkan kode fromJson dan toJson secara otomatis dari anotasi — menghilangkan boilerplate dan mengurangi risiko kesalahan ketik.
Instalasi #
# pubspec.yaml
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.13
json_serializable: ^6.8.0
Definisi Model #
// produk.dart
import 'package:json_annotation/json_annotation.dart';
part 'produk.g.dart'; // file yang akan di-generate
@JsonSerializable()
class Produk {
final String id;
final String nama;
final double harga;
final bool tersedia;
final List<String> kategori;
// @JsonKey: peta nama field JSON yang berbeda dengan nama field Dart
@JsonKey(name: 'created_at')
final DateTime createdAt;
// defaultValue: nilai default jika key tidak ada di JSON
@JsonKey(defaultValue: false)
final bool isFeatured;
const Produk({
required this.id,
required this.nama,
required this.harga,
required this.tersedia,
required this.kategori,
required this.createdAt,
this.isFeatured = false,
});
// fromJson dan toJson di-generate otomatis
factory Produk.fromJson(Map<String, dynamic> json) => _$ProdukFromJson(json);
Map<String, dynamic> toJson() => _$ProdukToJson(this);
}
Jalankan Code Generation #
# Generate sekali
flutter pub run build_runner build --delete-conflicting-outputs
# Watch mode -- auto-regenerate saat file berubah
flutter pub run build_runner watch --delete-conflicting-outputs
Perintah ini menghasilkan produk.g.dart berisi implementasi _$ProdukFromJson dan _$ProdukToJson.
@JsonKey — Kustomisasi Field #
@JsonSerializable()
class UserProfile {
// Nama field berbeda antara JSON dan Dart
@JsonKey(name: 'full_name')
final String fullName;
// Ignore field ini saat serialisasi/deserialisasi
@JsonKey(includeFromJson: false, includeToJson: false)
final bool isSelected;
// Nilai default jika key tidak ada
@JsonKey(defaultValue: 0)
final int loginCount;
// Custom converter untuk tipe yang tidak didukung langsung
@JsonKey(fromJson: _parseDate, toJson: _formatDate)
final DateTime createdAt;
static DateTime _parseDate(String date) => DateTime.parse(date);
static String _formatDate(DateTime date) => date.toIso8601String();
const UserProfile({
required this.fullName,
this.isSelected = false,
this.loginCount = 0,
required this.createdAt,
});
factory UserProfile.fromJson(Map<String, dynamic> json) =>
_$UserProfileFromJson(json);
Map<String, dynamic> toJson() => _$UserProfileToJson(this);
}
Freezed — Model Immutable dengan Semua Fitur #
Freezed menggabungkan kekuatan json_serializable dengan fitur class immutable yang lengkap: copyWith, == override, hashCode, toString, dan union types.
Instalasi #
# pubspec.yaml
dependencies:
freezed_annotation: ^2.4.4
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.13
freezed: ^2.5.7
json_serializable: ^6.8.0
Model Dasar dengan Freezed #
// produk.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'produk.freezed.dart';
part 'produk.g.dart';
@freezed
class Produk with _$Produk {
const factory Produk({
required String id,
required String nama,
required double harga,
@Default(true) bool tersedia,
@Default([]) List<String> kategori,
@JsonKey(name: 'created_at') required DateTime createdAt,
}) = _Produk;
factory Produk.fromJson(Map<String, dynamic> json) => _$ProdukFromJson(json);
}
// Freezed menghasilkan secara otomatis:
// - copyWith()
// - == dan hashCode
// - toString()
// - fromJson / toJson
// Penggunaan
final produk = Produk(
id: '1',
nama: 'Flutter Book',
harga: 150000,
createdAt: DateTime.now(),
);
// copyWith -- buat salinan dengan beberapa field diubah
final produkDiskon = produk.copyWith(harga: 120000);
// == bekerja berdasarkan nilai
print(produk == produk.copyWith()); // true!
Nested Object #
@freezed
class Pesanan with _$Pesanan {
const factory Pesanan({
required String id,
required Pelanggan pelanggan, // nested Freezed object
required List<ItemPesanan> items, // list of Freezed objects
required AlamatPengiriman alamat,
required StatusPesanan status, // enum
}) = _Pesanan;
factory Pesanan.fromJson(Map<String, dynamic> json) => _$PesananFromJson(json);
}
@freezed
class Pelanggan with _$Pelanggan {
const factory Pelanggan({
required String id,
required String nama,
required String email,
}) = _Pelanggan;
factory Pelanggan.fromJson(Map<String, dynamic> json) => _$PelangganFromJson(json);
}
enum StatusPesanan { menunggu, diproses, dikirim, selesai, dibatalkan }
Penting untuk nested Freezed objects: Tambahkan
@JsonSerializable(explicitToJson: true)jika Freezed class mengandung nested Freezed object, agartoJson()memanggiltoJson()pada nested object secara rekursif.@Freezed(toJson: true) // atau gunakan build.yaml: // targets: // $default: // builders: // json_serializable: // options: // explicit_to_json: true
Union Types — Satu Class, Banyak Bentuk #
Freezed memungkinkan satu class merepresentasikan beberapa kemungkinan state — sangat berguna untuk state networking:
@freezed
class HasilApi<T> with _$HasilApi<T> {
const factory HasilApi.sukses(T data) = _Sukses;
const factory HasilApi.error(String pesan, int? statusCode) = _Error;
const factory HasilApi.loading() = _Loading;
}
// Penggunaan dengan pattern matching
final hasil = HasilApi<List<Produk>>.sukses(produkList);
hasil.when(
sukses: (data) => print('${data.length} produk'),
error: (pesan, code) => print('Error $code: $pesan'),
loading: () => print('Memuat...'),
);
// maybeWhen -- hanya handle beberapa case
hasil.maybeWhen(
sukses: (data) => print('Sukses!'),
orElse: () => print('Bukan sukses'),
);
Generic API Response #
Server sering mengembalikan response dengan struktur yang seragam, hanya data-nya yang berbeda. Pola generic response menghindari duplikasi:
// Struktur umum API response:
// {
// "success": true,
// "message": "OK",
// "data": { ... } // atau [ ... ]
// }
@JsonSerializable(genericArgumentFactories: true)
class ApiResponse<T> {
final bool success;
final String message;
final T? data;
const ApiResponse({
required this.success,
required this.message,
this.data,
});
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT, // converter untuk tipe T
) => _$ApiResponseFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$ApiResponseToJson(this, toJsonT);
}
// Penggunaan
final response = ApiResponse<Produk>.fromJson(
jsonDecode(responseBody),
(json) => Produk.fromJson(json as Map<String, dynamic>),
);
final responseList = ApiResponse<List<Produk>>.fromJson(
jsonDecode(responseBody),
(json) => (json as List).map((e) => Produk.fromJson(e)).toList(),
);
// Mengakses data
if (response.success && response.data != null) {
final produk = response.data!;
}
JsonConverter — Konversi Tipe Kustom #
JsonConverter berguna saat server mengirim tipe yang tidak cocok langsung dengan Dart:
// Server kirim: "harga": "150000" (String, bukan double)
// Kita butuh: double
class StringToDoubleConverter implements JsonConverter<double, dynamic> {
const StringToDoubleConverter();
@override
double fromJson(dynamic json) {
if (json is num) return json.toDouble();
return double.parse(json.toString());
}
@override
dynamic toJson(double object) => object;
}
// Server kirim: "status": 1 (int)
// Kita butuh: enum StatusPesanan
class StatusConverter implements JsonConverter<StatusPesanan, int> {
const StatusConverter();
@override
StatusPesanan fromJson(int json) => StatusPesanan.values[json];
@override
int toJson(StatusPesanan object) => object.index;
}
// Penggunaan di model
@freezed
class Produk with _$Produk {
const factory Produk({
@StringToDoubleConverter() required double harga,
@StatusConverter() required StatusPesanan status,
}) = _Produk;
factory Produk.fromJson(Map<String, dynamic> json) => _$ProdukFromJson(json);
}
Perbandingan Pendekatan #
| Manual | json_serializable | Freezed | |
|---|---|---|---|
| Setup | Tidak perlu | build_runner | build_runner |
| Boilerplate | Banyak | Minimal | Minimal |
| fromJson/toJson | Manual | Generated | Generated |
| copyWith | Manual | Tidak ada | Generated |
| == dan hashCode | Manual | Tidak ada | Generated |
| toString | Manual | Tidak ada | Generated |
| Union types | Tidak ada | Tidak ada | Ada |
| Testability | Baik | Baik | Sangat Baik |
| Cocok untuk | Prototipe | Model sederhana | Semua skenario |
Ringkasan #
- Parsing manual dengan
dart:convertcukup untuk prototipe atau beberapa class sederhana — tapi tidak skala untuk aplikasi yang besar.json_serializablemenghasilkanfromJson/toJsonsecara otomatis — menghilangkan boilerplate dan mencegah typo. Gunakan@JsonKeyuntuk memetakan nama field yang berbeda antara JSON dan Dart.- Freezed adalah pilihan terlengkap — ia menghasilkan
fromJson,toJson,copyWith,==,hashCode,toString, dan mendukung union types. Ini adalah standar industri untuk model di Flutter produksi.- Gunakan
@Defaultdi Freezed untuk nilai default field opsional, sehingga JSON yang tidak mengandung field tersebut tetap bisa diparsing.- Untuk nested Freezed objects, pastikan
explicitToJson: trueagartoJson()memanggiltoJson()pada objek nested secara rekursif.JsonConvertermenangani kasus di mana tipe dari server tidak cocok langsung dengan tipe Dart — misalnya angka yang dikirim sebagai String, atau enum yang dikirim sebagai integer.- Selalu jalankan
build_runnersetelah mengubah class yang menggunakan anotasi. Gunakanwatchmode selama development.