JSON adalah format pertukaran data yang paling umum di antara layanan web, dan circe adalah pustaka JSON untuk Scala yang menempatkan kemampuan menyandikan dan membaca JSON pada type class Encoder dan Decoder. Topik ini membahas cara memperoleh kedua instance itu secara otomatis dari struktur case class melalui derivasi (derivation) milik circe-generic. Pembahasan juga mencakup penyandian nilai menjadi string JSON dengan asJson, pembacaan kembali dengan parser.decode yang menghasilkan Either, pencetakan JSON secara rapi dengan spaces2, serta pembacaan satu bidang tertentu melalui cursor. Setiap contoh dijalankan dengan scala-cli dan tiga artefak circe yang versinya dipatok pada 0.14.13.
Derivasi otomatis dan penyandian#
Derivasi otomatis menyusun instance Encoder dan Decoder dari struktur case class, sehingga tidak ada kode penyandian yang ditulis secara manual. Import io.circe.generic.auto.* menyediakan instance tersebut untuk setiap case class yang muncul pada ekspresi, termasuk case class yang berada di dalam List atau Option. Metode asJson, yang berasal dari io.circe.syntax.*, mengubah nilai menjadi objek Json, lalu noSpaces merendernya menjadi string tanpa spasi. Program berikut menyandikan sebuah Rak berisi dua Buku, mencetak JSON-nya, lalu membacanya kembali menjadi case class.
//> using dep io.circe::circe-core:0.14.13
//> using dep io.circe::circe-generic:0.14.13
//> using dep io.circe::circe-parser:0.14.13
// Encoder dan Decoder untuk case class diderivasi otomatis oleh circe-generic
import io.circe.generic.auto.*
import io.circe.parser
import io.circe.syntax.*
case class Buku(judul: String, tahun: Int)
case class Rak(label: String, buku: List[Buku])
@main def kirim(): Unit =
val rak = Rak("rak-utama", List(Buku("Pengantar Scala", 2023), Buku("Struktur Data", 2021)))
// Menyandikan nilai case class menjadi string JSON tanpa spasi
val json: String = rak.asJson.noSpaces
println(json)
// Membaca string JSON kembali menjadi case class, hasilnya Either
val kembali: Either[io.circe.Error, Rak] = parser.decode[Rak](json)
kembali match
case Right(hasil) => println(s"Diterima: ${hasil.label}, ${hasil.buku.length} buku")
case Left(error) => println(s"Decode error: $error")# menjalankan program kirim.scala
scala-cli run kirim.scala{"label":"rak-utama","buku":[{"judul":"Pengantar Scala","tahun":2023},{"judul":"Struktur Data","tahun":2021}]}
Diterima: rak-utama, 2 bukuKeluaran pertama menunjukkan urutan bidang JSON mengikuti urutan deklarasi bidang pada case class, yaitu label lebih dahulu dan buku kemudian. Pemanggilan parser.decode[Rak](json) mengembalikan Either[io.circe.Error, Rak], di mana io.circe.Error adalah tipe induk untuk dua jenis kegagalan, yaitu ParsingFailure pada tingkat teks dan DecodingFailure pada tingkat struktur. Kedua sisi Either ditangani dengan pencocokan pola, sehingga keberhasilan pembacaan terlihat dari Right. Nilai hasil decode sama sekali tidak berbagi struktur dengan string JSON-nya, melainkan berupa objek case class yang siap digunakan seperti objek Scala biasa.
Pencetakan rapi dan kursor#
Pembacaan JSON dari teks dimulai dengan parser.parse, yang menghasilkan Either[ParsingFailure, Json]. Objek Json adalah representasi pohon dari data, dan printer spaces2 merendernya dengan indentasi dua spasi. Untuk membaca sebagian isi, json.hcursor menghasilkan cursor, yaitu penunjuk posisi pada pohon JSON, yang dapat berpindah dengan downField("nama") menuju satu bidang objek atau downArray menuju elemen pertama sebuah array. Metode as[T] pada cursor mengubah nilai pada posisi tersebut menjadi tipe T dan menghasilkan Either[DecodingFailure, T], sehingga pembacaan bidang tidak pernah melempar exception.
//> using dep io.circe::circe-core:0.14.13
//> using dep io.circe::circe-parser:0.14.13
// spaces2 mencetak JSON rapi, hcursor membaca bidang tertentu
import io.circe.Json
import io.circe.parser
@main def rapi(): Unit =
val teks = """{"sensor":"suhu-ruang","bacaan":[24,25,23]}"""
parser.parse(teks) match
case Right(json) =>
println(json.spaces2)
val kursor = json.hcursor
val nama = kursor.downField("sensor").as[String]
val bacaan = kursor.downField("bacaan").as[List[Int]]
println(s"Sensor: ${nama.getOrElse("(tidak terbaca)")}")
println(s"Jumlah bacaan: ${bacaan.getOrElse(Nil).length}")
case Left(parsingError) =>
println(s"Parse error: ${parsingError.message}")# menjalankan program rapi.scala
scala-cli run rapi.scala{
"sensor" : "suhu-ruang",
"bacaan" : [
24,
25,
23
]
}
Sensor: suhu-ruang
Jumlah bacaan: 3Keluaran menunjukkan gaya printer spaces2, yaitu titik dua ditulis dengan spasi di kedua sisinya dan array dibuka pada baris tersendiri. Pada cursor, downField("sensor").as[String] menghasilkan Right("suhu-ruang"), sedangkan downField("bacaan").as[List[Int]] berhasil karena setiap elemen array memang angka. Metode getOrElse pada Either menyediakan nilai pengganti apabila hasilnya Left, sehingga program tetap berjalan dengan nilai default alih-alih berhenti.
Ketika JSON tidak cocok#
Kegagalan decode tidak berbentuk exception, melainkan Left yang berisi DecodingFailure beserta lokasi bidangnya. Dua kegagalan yang paling sering terjadi adalah tipe yang tidak cocok dan bidang yang hilang. String JSON berikut memiliki umur bertipe teks padahal case class meminta Int, dan string kedua sama sekali tidak memiliki bidang umur. Program berikut mencoba keduanya dan mencetak hasil decode mentahnya.
//> using dep io.circe::circe-core:0.14.13
//> using dep io.circe::circe-generic:0.14.13
//> using dep io.circe::circe-parser:0.14.13
// JSON yang tidak cocok dengan struktur case class menghasilkan Left
import io.circe.generic.auto.*
import io.circe.parser
case class Siswa(nama: String, umur: Int)
@main def tidakCocok(): Unit =
val salahTipe = """{"nama":"Budi","umur":"tujuh belas"}"""
val kurangBidang = """{"nama":"Budi"}"""
List(salahTipe, kurangBidang).foreach { teks =>
parser.decode[Siswa](teks) match
case Right(siswa) => println(s"Diterima: ${siswa.nama}, umur ${siswa.umur}")
case Left(error) => println(s"Decode error: $error")
}# menjalankan program tidak-cocok.scala
scala-cli run tidak-cocok.scalaDecode error: DecodingFailure at .umur: Int
Decode error: DecodingFailure at .umur: Missing required fieldKedua pesan menunjuk jalur yang sama, yaitu .umur, karena kegagalannya berada pada bidang itu. Pesan Int berarti nilai pada bidang tersebut tidak dapat dibaca sebagai angka bertipe Int, sedangkan pesan Missing required field berarti bidang itu tidak ditemukan sama sekali. Bentuk Either memungkinkan pemanggil menentukan sendiri reaksinya, misalnya memberi nilai bawaan, mengumpulkan semua kegagalan, atau meneruskan pesan error ke pemanggil layanannya.
Jalur pada pesan error mengikuti kedalaman struktur data. Ketika case class memuat case class lain, kegagalan pada bidang yang bersarang dilaporkan dengan jalur penuh, misalnya .siswa.umur untuk bidang umur yang berada di dalam objek siswa. Bentuk jalur ini membuat pesan error tetap dapat ditindaklanjuti pada dokumen JSON yang panjang, karena lokasi bidang yang bermasalah disebut dari akar dokumen sampai titik kegagalannya, bukan hanya nama bidangnya.
Kesalahan umum#
- Artefak
circe-generictidak disertakan pada direktif//> using dep. Kompilasi gagal karena instanceEncoderdanDecoderuntuk case class tidak dapat disusun. Perbaikannya adalah menambahkanio.circe::circe-generic:0.14.13di samping circe-core dan circe-parser. - Import
io.circe.syntax.*tidak disertakan. MetodeasJsontidak ditemukan pada case class karena metode itu merupakan metode ekstensi pada paketsyntax. Perbaikannya adalah menambahkan import tersebut di bagian atas berkas. - Decode diasumsikan melempar exception apabila data salah. Hasil
parser.decodememang berupaEither, sehingga cabangLeftyang tidak ditangani tidak menyebabkan program berhenti, tetapi membuat nilai hasil hilang tanpa sinyal. Perbaikannya adalah selalu menangani kedua cabang pada pemanggil.
Ringkasan#
- Derivasi
io.circe.generic.auto.*menyusun instanceEncoderdanDecoderdari struktur case class, sehinggaasJsondanparser.decodedapat digunakan tanpa menulis kode penyandian manual. - Penyandian menghasilkan string JSON dengan urutan bidang sesuai deklarasi bidang, sedangkan pembacaan menghasilkan
Either[io.circe.Error, T]yang menampungParsingFailuremaupunDecodingFailure. spaces2mencetak JSON dengan indentasi untuk dibaca manusia, danhcursordengandownFieldmembaca bidang tertentu yang hasilnya jugaEither.- Kegagalan decode berupa
LeftberisiDecodingFailureyang menunjuk lokasi bidangnya, bukan exception.
Referensi#
- circe, JSON for Scala — proyek circe — Dokumentasi resmi circe yang mencakup derivasi codec, parsing, dan printer.
- Automatic derivation — circe — Penjelasan resmi derivasi otomatis instance codec dari struktur case class.
- Cursors — circe
— Dokumentasi resmi
HCursordan navigasidownFielddi atas pohon JSON.