Hampir semua API web modern bertukar data dalam format JSON. Package encoding/json pada standard library Go menyediakan mesin dua arah untuk format tersebut: json.Marshal mengubah nilai Go menjadi teks JSON, sedangkan json.Unmarshal mengubah teks JSON kembali menjadi nilai Go. Pada chapter ini kita mempraktikkan kedua arah pada struct, slice, dan map, lalu menangkap galat decode dengan errors.As.

Marshal: Mengubah Nilai Go Menjadi JSON #

Fungsi json.Marshal menerima satu nilai bertipe any, membaca struktur nilai lewat reflection, lalu mengembalikan []byte berisi teks JSON. Ketika nilainya struct, json.Marshal hanya membaca field exported dan nama field output mengikuti nama field Go apa adanya:

go
data, err := json.Marshal(pengguna)

Pemanggilan di atas menghasilkan JSON satu baris rapat, sedangkan json.MarshalIndent(pengguna, "", " ") menambahkan baris baru dan indentasi dua spasi. Tabel berikut merangkum pintu masuk utama package encoding/json:

APIArahKapan dipakai
json.Marshal / json.MarshalIndentGo → JSONData sudah utuh di memori
json.UnmarshalJSON → GoTeks JSON sudah utuh di memori
json.NewEncoder / json.NewDecoderDua arah, streamData besar atau datang bertahap
json.RawMessageMenunda decodeBagian dokumen diurai belakangan

Program json-demo.go berikut memperagakan MarshalIndent dan Unmarshal pada struct bersarang, slice, map, field unexported, decode dinamis, dan galat decode:

go
package main

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

type Alamat struct {
	Kota    string
	KodePos string
}

type Pengguna struct {
	Nama      string
	Umur      int
	Email     string
	Alamat    Alamat
	Hobi      []string
	Pekerjaan map[string]string
}

type Akun struct {
	Nama  string
	sandi string // unexported: tidak pernah ikut ke JSON
}

func main() {
	p := Pengguna{
		Nama:   "Budi",
		Umur:   25,
		Email:  "[email protected]",
		Alamat: Alamat{Kota: "Bandung", KodePos: "40115"},
		Hobi:   []string{"membaca", "bersepeda"},
		Pekerjaan: map[string]string{
			"perusahaan": "PT Maju Bersama",
			"posisi":     "Backend Engineer",
		},
	}

	data, err := json.MarshalIndent(p, "", "  ")
	if err != nil {
		fmt.Println("marshal gagal:", err)
		return
	}
	fmt.Println(string(data))

	sumber := []byte(`{
		"Nama": "Siti",
		"Umur": 28,
		"Email": "[email protected]",
		"Alamat": {"Kota": "Surabaya", "KodePos": "60111"},
		"Hobi": ["memasak", "menjahit"],
		"Pekerjaan": {"posisi": "Frontend Engineer"},
		"Telepon": "0812-0000-0000"
	}`)

	var p2 Pengguna
	if err := json.Unmarshal(sumber, &p2); err != nil {
		fmt.Println("unmarshal gagal:", err)
		return
	}
	fmt.Printf("hasil decode: %+v\n", p2)

	dataAkun, _ := json.Marshal(Akun{Nama: "Siti", sandi: "rahasia-123"})
	fmt.Println("akun:", string(dataAkun))

	var dinamis map[string]any
	json.Unmarshal([]byte(`{"umur": 25, "aktif": true, "nama": "Budi"}`), &dinamis)
	for _, k := range []string{"nama", "umur", "aktif"} {
		fmt.Printf("dinamis[%s] = %v bertipe %T\n", k, dinamis[k], dinamis[k])
	}

	var galatTipe Pengguna
	err1 := json.Unmarshal([]byte(`{"Umur":"dua puluh lima"}`), &galatTipe)
	fmt.Println("galat 1:", err1)
	var targetTipe *json.UnmarshalTypeError
	fmt.Println("galat 1 adalah UnmarshalTypeError?", errors.As(err1, &targetTipe))

	var galatSintaks Pengguna
	err2 := json.Unmarshal([]byte(`{"Nama":`), &galatSintaks)
	fmt.Println("galat 2:", err2)
	var targetSintaks *json.SyntaxError
	fmt.Println("galat 2 adalah SyntaxError?", errors.As(err2, &targetSintaks))
}

Output yang Dihasilkan:

text
{
  "Nama": "Budi",
  "Umur": 25,
  "Email": "[email protected]",
  "Alamat": {
    "Kota": "Bandung",
    "KodePos": "40115"
  },
  "Hobi": [
    "membaca",
    "bersepeda"
  ],
  "Pekerjaan": {
    "perusahaan": "PT Maju Bersama",
    "posisi": "Backend Engineer"
  }
}
hasil decode: {Nama:Siti Umur:28 Email:[email protected] Alamat:{Kota:Surabaya KodePos:60111} Hobi:[memasak menjahit] Pekerjaan:map[posisi:Frontend Engineer]}
akun: {"Nama":"Siti"}
dinamis[nama] = Budi bertipe string
dinamis[umur] = 25 bertipe float64
dinamis[aktif] = true bertipe bool
galat 1: json: cannot unmarshal string into Go struct field Pengguna.Umur of type int
galat 1 adalah UnmarshalTypeError? true
galat 2: unexpected end of JSON input
galat 2 adalah SyntaxError? true

Mari kita telaah baris demi baris program di atas:

  1. Baris 9-21 (type Alamat dan Pengguna): dua struct bersarang; Hobi menjadi array JSON dan Pekerjaan menjadi objek JSON berkunci terurut.
  2. Baris 23-26 (type Akun): struct uji dengan field sandi unexported.
  3. Baris 41-46: json.MarshalIndent mengubah p menjadi JSON berindentasi lalu mencetaknya sebagai teks.
  4. Baris 48-56 (sumber): teks JSON masukan yang memuat field Telepon, field yang tidak ada pada struct Pengguna.
  5. Baris 58-63: json.Unmarshal(sumber, &p2) mengisi struct lewat pointer; field Telepon diabaikan tanpa galat.
  6. Baris 65-66: memarshal Akun; output hanya memuat {"Nama":"Siti"} karena sandi tidak dibaca.
  7. Baris 68-72: decode JSON ke map[string]any; verb %T mencetak tipe tiap nilai.
  8. Baris 74-84: dua galat dipancing lalu diperiksa memakai errors.As dengan target *json.UnmarshalTypeError dan *json.SyntaxError.

Field Unexported Hilang Diam-diam #

Output akun: {"Nama":"Siti"} membuktikan field sandi lenyap tanpa galat. Package encoding/json hanya mengakses field lewat reflection untuk identifier exported, sehingga field berhuruf kecil di awal nama tidak pernah dibaca maupun ditulis. Perilaku sama berlaku pada gob dan XML.

Decode Dinamis: any dan Jebakan float64 #

Ketika bentuk JSON tidak diketahui di muka, kita bisa mendecode ke map[string]any. Baris output dinamis[umur] = 25 bertipe float64 memperlihatkan jebakan klasik: setiap angka JSON menjadi float64, bukan int.

Galat Decode dan Jenisnya #

Galat json: cannot unmarshal string into Go struct field Pengguna.Umur of type int bertipe *json.UnmarshalTypeError: nilai JSON tidak cocok dengan tipe field tujuan. Galat unexpected end of JSON input bertipe *json.SyntaxError: teks JSON rusak sintaksnya. Dengan errors.As kita bisa membedakan masalah muatan dari masalah transport, sejalan dengan materi errors.Is dan errors.As .

Stream: Decoder, Encoder, dan RawMessage #

json.Unmarshal membutuhkan seluruh teks JSON sekaligus di memori. Untuk data besar, json.NewDecoder membaca objek demi objek dari io.Reader, json.NewEncoder menulis objek demi objek ke io.Writer, dan json.RawMessage menunda decode bagian yang belum perlu diurai. Program json-stream-demo.go memperagakan ketiganya:

go
package main

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

type Barang struct {
	ID   string
	Nama string
	Stok int
}

func main() {
	sumber := `{"ID":"B-01","Nama":"Kopi","Stok":12}
{"ID":"B-02","Nama":"Teh","Stok":8}
{"ID":"B-03","Nama":"Gula","Stok":5}`

	dec := json.NewDecoder(strings.NewReader(sumber))
	for {
		var b Barang
		err := dec.Decode(&b)
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("decode gagal:", err)
			return
		}
		fmt.Printf("baca stream: %+v\n", b)
	}

	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetIndent("", "  ")
	if err := enc.Encode(Barang{ID: "B-01", Nama: "Kopi", Stok: 12}); err != nil {
		fmt.Println("encode gagal:", err)
		return
	}
	if err := enc.Encode(Barang{ID: "B-02", Nama: "Teh", Stok: 8}); err != nil {
		fmt.Println("encode gagal:", err)
		return
	}
	fmt.Print(buf.String())

	dokumen := `{"id":"T-9","total":3,"detail":{"panjang":14,"lebar":6}}`
	var mentah map[string]json.RawMessage
	if err := json.Unmarshal([]byte(dokumen), &mentah); err != nil {
		fmt.Println("unmarshal gagal:", err)
		return
	}
	var id string
	json.Unmarshal(mentah["id"], &id)
	fmt.Println("id:", id)
	fmt.Printf("detail mentah: %s\n", mentah["detail"])
}

Output yang Dihasilkan:

text
baca stream: {ID:B-01 Nama:Kopi Stok:12}
baca stream: {ID:B-02 Nama:Teh Stok:8}
baca stream: {ID:B-03 Nama:Gula Stok:5}
{
  "ID": "B-01",
  "Nama": "Kopi",
  "Stok": 12
}
{
  "ID": "B-02",
  "Nama": "Teh",
  "Stok": 8
}
id: T-9
detail mentah: {"panjang":14,"lebar":6}

Mari kita telaah baris demi baris program di atas:

  1. Baris 18-20 (sumber): tiga objek JSON berjejer dalam satu teks; json.Unmarshal tidak bisa memrosesnya sekaligus.
  2. Baris 22-34: json.NewDecoder membungkus strings.NewReader; loop dec.Decode(&b) berjalan sampai io.EOF.
  3. Baris 36-38: json.NewEncoder dibuat di atas bytes.Buffer; enc.SetIndent mengatur indentasi.
  4. Baris 39-47: dua pemanggilan enc.Encode menulis dua objek JSON terpisah.
  5. Baris 49-58: dokumen didecode ke map[string]json.RawMessage; id diurai belakangan, detail tetap mentah.

Referensi #

Setelah menguasai JSON, kita berpindah ke format lama yang masih dipakai luas di sistem enterprise: XML pada chapter berikutnya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

json-demo.go

Unduh berkas
go
package main

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

type Alamat struct {
	Kota    string
	KodePos string
}

type Pengguna struct {
	Nama      string
	Umur      int
	Email     string
	Alamat    Alamat
	Hobi      []string
	Pekerjaan map[string]string
}

type Akun struct {
	Nama  string
	sandi string // unexported: tidak pernah ikut ke JSON
}

func main() {
	p := Pengguna{
		Nama:   "Budi",
		Umur:   25,
		Email:  "[email protected]",
		Alamat: Alamat{Kota: "Bandung", KodePos: "40115"},
		Hobi:   []string{"membaca", "bersepeda"},
		Pekerjaan: map[string]string{
			"perusahaan": "PT Maju Bersama",
			"posisi":     "Backend Engineer",
		},
	}

	data, err := json.MarshalIndent(p, "", "  ")
	if err != nil {
		fmt.Println("marshal gagal:", err)
		return
	}
	fmt.Println(string(data))

	sumber := []byte(`{
		"Nama": "Siti",
		"Umur": 28,
		"Email": "[email protected]",
		"Alamat": {"Kota": "Surabaya", "KodePos": "60111"},
		"Hobi": ["memasak", "menjahit"],
		"Pekerjaan": {"posisi": "Frontend Engineer"},
		"Telepon": "0812-0000-0000"
	}`)

	var p2 Pengguna
	if err := json.Unmarshal(sumber, &p2); err != nil {
		fmt.Println("unmarshal gagal:", err)
		return
	}
	fmt.Printf("hasil decode: %+v\n", p2)

	dataAkun, _ := json.Marshal(Akun{Nama: "Siti", sandi: "rahasia-123"})
	fmt.Println("akun:", string(dataAkun))

	var dinamis map[string]any
	json.Unmarshal([]byte(`{"umur": 25, "aktif": true, "nama": "Budi"}`), &dinamis)
	for _, k := range []string{"nama", "umur", "aktif"} {
		fmt.Printf("dinamis[%s] = %v bertipe %T\n", k, dinamis[k], dinamis[k])
	}

	var galatTipe Pengguna
	err1 := json.Unmarshal([]byte(`{"Umur":"dua puluh lima"}`), &galatTipe)
	fmt.Println("galat 1:", err1)
	var targetTipe *json.UnmarshalTypeError
	fmt.Println("galat 1 adalah UnmarshalTypeError?", errors.As(err1, &targetTipe))

	var galatSintaks Pengguna
	err2 := json.Unmarshal([]byte(`{"Nama":`), &galatSintaks)
	fmt.Println("galat 2:", err2)
	var targetSintaks *json.SyntaxError
	fmt.Println("galat 2 adalah SyntaxError?", errors.As(err2, &targetSintaks))
}

json-stream-demo.go

Unduh berkas
go
package main

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

type Barang struct {
	ID   string
	Nama string
	Stok int
}

func main() {
	sumber := `{"ID":"B-01","Nama":"Kopi","Stok":12}
{"ID":"B-02","Nama":"Teh","Stok":8}
{"ID":"B-03","Nama":"Gula","Stok":5}`

	dec := json.NewDecoder(strings.NewReader(sumber))
	for {
		var b Barang
		err := dec.Decode(&b)
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("decode gagal:", err)
			return
		}
		fmt.Printf("baca stream: %+v\n", b)
	}

	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetIndent("", "  ")
	if err := enc.Encode(Barang{ID: "B-01", Nama: "Kopi", Stok: 12}); err != nil {
		fmt.Println("encode gagal:", err)
		return
	}
	if err := enc.Encode(Barang{ID: "B-02", Nama: "Teh", Stok: 8}); err != nil {
		fmt.Println("encode gagal:", err)
		return
	}
	fmt.Print(buf.String())

	dokumen := `{"id":"T-9","total":3,"detail":{"panjang":14,"lebar":6}}`
	var mentah map[string]json.RawMessage
	if err := json.Unmarshal([]byte(dokumen), &mentah); err != nil {
		fmt.Println("unmarshal gagal:", err)
		return
	}
	var id string
	json.Unmarshal(mentah["id"], &id)
	fmt.Println("id:", id)
	fmt.Printf("detail mentah: %s\n", mentah["detail"])
}