encoding/json

Marshal dan unmarshal serta dekoder streaming.

Daftar isi

Paket encoding/json mengubah nilai Go menjadi dokumen JSON dan sebaliknya, sebuah proses yang disebut marshal untuk penyandian dan unmarshal untuk pembacaan kembali. Topik ini membahas penyandian struct beserta tag JSON-nya, pembacaan dokumen JSON ke struct, dan dekoder aliran yang memproses dokumen JSON satu per satu tanpa memuat semuanya sekaligus. Contohnya memakai catatan gudang berisi judul, status, dan nomor urut, dengan keluaran yang tetap karena urutan field struct menentukan urutan kunci JSON. Setelah membacanya, pertukaran data JSON dengan program lain dapat dilakukan dengan tipe yang benar dan penanganan error yang lengkap.

Struct, tag, dan marshal#

Fungsi json.Marshal menyandikan nilai Go menjadi []byte berisi JSON, sedangkan json.MarshalIndent menyandikannya dengan indentasi agar mudah dibaca. Nama kunci JSON mengikuti nama field struct secara default, tetapi nama itu biasanya diubah melalui tag, yaitu anotasi bertanda kutip belakang yang ditempel pada field dan dituliskan dalam bentuk json:"nama". Tag juga dipakai untuk melewatkan field dengan tanda -. Urutan kunci pada hasil mengikuti urutan field pada struct, sehingga keluaran program yang sama selalu identik. Tipe map juga dapat disandikan, dan kuncinya diurutkan secara alfabetis agar hasilnya tetap dapat diprediksi. Program berikut menyandikan satu struct catatan dan satu peta penghitung.

go catatan-json.go
package main

import (
	"encoding/json"
	"fmt"
)

type Catatan struct {
	Judul  string `json:"judul"`
	Status string `json:"status"`
	Urut   int    `json:"urut"`
}

func main() {
	c := Catatan{Judul: "Laporan Gudang", Status: "draf", Urut: 7}
	b, err := json.MarshalIndent(c, "", "  ")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(string(b))

	penghitung := map[string]int{"cabang-c": 3, "cabang-a": 1, "cabang-b": 2}
	m, err := json.Marshal(penghitung)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(string(m))
}
text Keluaran
{
  "judul": "Laporan Gudang",
  "status": "draf",
  "urut": 7
}
{"cabang-a":1,"cabang-b":2,"cabang-c":3}

Keluaran di atas memperlihatkan dua sifat yang saling melengkapi. Struct Catatan disandikan sesuai urutan fieldnya, yaitu judul, status, lalu nomor urut, sehingga hasilnya mudah dibandingkan antarversi program. Peta penghitung disandikan dengan kunci yang diurutkan dari cabang-a sampai cabang-c, meskipun deklarasinya menyebut urutan lain, karena peta di Go tidak memiliki urutan tersendiri. Tag "judul" dan seterusnya membuat nama kunci JSON ditulis dengan huruf kecil semua, mengikuti kebiasaan format JSON pada umumnya. Opsi tambahan dapat dilekatkan setelah nama kunci dengan pemisah koma, misalnya json:"status,omitempty" yang menghilangkan field tersebut dari hasil ketika nilainya nol.

Unmarshal JSON ke struct#

Fungsi json.Unmarshal membaca []byte berisi JSON dan mengisinya ke pointer struct yang diberikan. Pencocokan dilakukan berdasarkan nama kunci JSON terhadap tag field, tanpa memedulikan urutannya, dan field yang tidak ada pada dokumen dibiarkan bernilai nol. Error dikembalikan apabila dokumen bukan JSON yang sah atau tipe data tidak cocok dengan field tujuan, misalnya teks yang diisikan ke field int. Program berikut membaca satu dokumen lengkap dan satu dokumen yang hanya memuat sebagian fieldnya.

go baca-json.go
package main

import (
	"encoding/json"
	"fmt"
)

type Catatan struct {
	Judul  string `json:"judul"`
	Status string `json:"status"`
	Urut   int    `json:"urut"`
}

func main() {
	dokumen := `{"judul":"Laporan Gudang","status":"terkirim","urut":7}`
	var c Catatan
	if err := json.Unmarshal([]byte(dokumen), &c); err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(c.Judul, c.Status, c.Urut)

	sebagian := `{"judul":"Catatan Singkat"}`
	var k Catatan
	if err := json.Unmarshal([]byte(sebagian), &k); err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Printf("%q %q %d\n", k.Judul, k.Status, k.Urut)
}
text Keluaran
Laporan Gudang terkirim 7
"Catatan Singkat" "" 0

Dokumen kedua hanya memuat kunci judul, sehingga field Status dan Urut tetap berupa nilai nol bertipe masing-masing, yaitu string kosong dan angka 0. Perilaku ini menegaskan bahwa Unmarshal tidak menuntut dokumen yang lengkap, tetapi juga tidak memberi tahu field mana yang hilang, sehingga field yang wajib ada harus diperiksa sendiri setelah pembacaan selesai.

Dekoder streaming dengan json.Decoder#

Untuk aliran dokumen JSON yang berkelanjutan, misalnya satu dokumen per baris dari log atau koneksi jaringan, json.Decoder membaca satu dokumen pada satu waktu melalui metode Decode. Pendekatan ini tidak menunggu seluruh data selesai, sehingga dokumen pertama dapat diproses ketika dokumen berikutnya masih dalam perjalanan. Metode More melaporkan apakah masih ada dokumen lain pada aliran tersebut. Program berikut memproses dua catatan yang ditulis berurutan pada satu aliran teks.

go dekoder-aliran.go
package main

import (
	"encoding/json"
	"fmt"
	"strings"
)

type Catatan struct {
	Judul string `json:"judul"`
	Urut  int    `json:"urut"`
}

func main() {
	aliran := strings.NewReader(
		`{"judul":"Draf laporan","urut":1}` + "\n" +
			`{"judul":"Laporan final","urut":2}` + "\n")

	dek := json.NewDecoder(aliran)
	for dek.More() {
		var c Catatan
		if err := dek.Decode(&c); err != nil {
			fmt.Println("error:", err)
			return
		}
		fmt.Printf("urut %d: %s\n", c.Urut, c.Judul)
	}
}
text Keluaran
urut 1: Draf laporan
urut 2: Laporan final

Fungsi json.NewDecoder menerima sembarang io.Reader, sehingga sumber alirannya dapat berupa berkas, koneksi jaringan, atau string pada contoh ini. Perulangan berhenti ketika More melaporkan aliran habis, dan setiap Decode mengembalikan error yang wajib diperiksa. Pola ini menjadi dasar pemrosesan log terstruktur dan respons bertahap dari server. Apabila aliran berupa satu array JSON tunggal, metode Token pada dekoder dapat melewati tanda kurung array sebelum dokumen-dokumen di dalamnya dibaca satu per satu. Pendekatan yang sama berlaku untuk respons server yang dikirim secara bertahap.

Ketiga program dapat dijalankan dari terminal sebagaimana berikut.

bash
# siapkan folder kerja untuk topik ini
mkdir -p /tmp/gopustaka/json
cp catatan-json.go baca-json.go dekoder-aliran.go /tmp/gopustaka/json/
cd /tmp/gopustaka/json

# jalankan program penyandian JSON
go run catatan-json.go

# jalankan program pembacaan JSON
go run baca-json.go

# jalankan program dekoder aliran
go run dekoder-aliran.go
text Keluaran
{
  "judul": "Laporan Gudang",
  "status": "draf",
  "urut": 7
}
{"cabang-a":1,"cabang-b":2,"cabang-c":3}
Laporan Gudang terkirim 7
"Catatan Singkat" "" 0
urut 1: Draf laporan
urut 2: Laporan final

Kesalahan umum#

  • Melewatkan field privat pada struct yang hendak disandikan. Field berawalan huruf kecil diabaikan oleh Marshal dan Unmarshal tanpa pesan error, sehingga datanya tidak pernah sampai. Perbaikannya, ekspor field tersebut atau salin isinya ke struct yang fieldnya diekspor.
  • Menulis tag tanpa tanda kutip belakang. Tag ditulis sebagai json:"judul" di dalam kutip belakang, dan bentuk seperti json="judul" tidak terbaca sebagai tag JSON. Perbaikannya, gunakan tanda kutip belakang dan awalan json:.
  • Mengabaikan error pada Unmarshal. Dokumen yang rusak menghasilkan struct dengan nilai nol, sehingga program berlanjut dengan data kosong. Perbaikannya, periksa error sebelum hasilnya dipakai.
  • Menganggap hasil unmarshal selalu lengkap. Field yang tidak ada pada dokumen dibiarkan bernilai nol tanpa pemberitahuan, sehingga data yang wajib ada dapat lolos begitu saja. Perbaikannya, periksa field wajib setelah pembacaan selesai sebelum nilainya dipakai.
  • Memakai Unmarshal untuk aliran panjang. Fungsi tersebut menuntut seluruh dokumen berada di memori sebagai []byte. Perbaikannya, gunakan json.Decoder yang membaca dari io.Reader secara bertahap.

Ringkasan#

  • json.Marshal dan json.MarshalIndent menyandikan struct sesuai urutan fieldnya, sedangkan peta disandikan dengan kunci yang diurutkan.
  • Tag json:"nama" mengganti nama kunci JSON dan hanya berlaku pada field yang diekspor.
  • json.Unmarshal mengisi struct dari dokumen JSON berdasarkan nama kunci, dengan field yang tidak ada dibiarkan bernilai nol.
  • json.Decoder memproses aliran dokumen JSON satu per satu melalui Decode dan More, sehingga cocok untuk data yang berkelanjutan.

Referensi#