json.Marshal memetakan struct ke JSON dengan aturan bawaan: nama field apa adanya, waktu menjadi string RFC 3339, angka menjadi angka. Aturan bawaan tersebut kadang tidak sesuai kontrak API yang sudah berjalan — format tanggal internal perusahaan, uang dalam satuan sen, atau field yang harus tampil sebagai string. Package encoding/json menyediakan dua interface untuk mengambil alih kendali: json.Marshaler dan json.Unmarshaler. Pada chapter ini kita akan mengimplementasikan keduanya pada tipe WaktuBeku dan Uang, lalu membedah masalah receiver pointer versus value.

Interface json.Marshaler dan json.Unmarshaler #

Kedua interface hanya berisi satu method:

go
MarshalJSON() ([]byte, error)
UnmarshalJSON(data []byte) error

Ketika sebuah tipe mengimplementasikan MarshalJSON, json.Marshal memanggil method tersebut dan memakai hasilnya sebagai representasi JSON tipe itu, mengabaikan field-field di dalamnya. Sebaliknya, UnmarshalJSON menerima potongan byte mentah milik field tersebut, menguraikannya sendiri, lalu mengisi nilainya. Method ini juga dipanggil oleh json.NewDecoder dan json.NewEncoder — seluruh jalur JSON menghormati kedua interface.

Tipe Waktu dengan Format Tetap #

Tipe WaktuBeku di bawah membungkus time.Time dan memaksa format "2006-01-02 15:04" di kedua arah. Tipe Uang menyimpan nilai dalam sen bertipe int64 agar perhitungan uang bebas pembulatan pecahan, lalu tampil sebagai angka sen polos di JSON. Program marshaler-demo.go berikut memperagakan round-trip lengkap:

go
package main

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

type WaktuBeku struct {
	Waktu time.Time
}

// MarshalJSON mengubah WaktuBeku menjadi string berformat "2006-01-02 15:04".
func (w WaktuBeku) MarshalJSON() ([]byte, error) {
	stempel := w.Waktu.Format("2006-01-02 15:04")
	return []byte(`"` + stempel + `"`), nil
}

// UnmarshalJSON mem-parsing string "2006-01-02 15:04" kembali menjadi waktu.
func (w *WaktuBeku) UnmarshalJSON(data []byte) error {
	t, err := time.Parse("2006-01-02 15:04", strings.Trim(string(data), `"`))
	if err != nil {
		return fmt.Errorf("WaktuBeku: format harus 2006-01-02 15:04: %w", err)
	}
	w.Waktu = t
	return nil
}

// Uang menyimpan nilai dalam satuan sen agar perhitungan tetap bulat.
type Uang struct {
	Sen int64
}

func (u Uang) MarshalJSON() ([]byte, error) {
	return []byte(strconv.FormatInt(u.Sen, 10)), nil
}

func (u *Uang) UnmarshalJSON(data []byte) error {
	n, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
	if err != nil {
		return fmt.Errorf("Uang: harus angka bulat sen: %w", err)
	}
	u.Sen = n
	return nil
}

type Transaksi struct {
	ID        string
	Waktu     WaktuBeku
	Jumlah    Uang
	Pelanggan string
}

func main() {
	t := Transaksi{
		ID:        "T-001",
		Waktu:     WaktuBeku{Waktu: time.Date(2026, 1, 15, 8, 30, 0, 0, time.UTC)},
		Jumlah:    Uang{Sen: 125050},
		Pelanggan: "Budi",
	}
	data, err := json.MarshalIndent(t, "", "  ")
	if err != nil {
		fmt.Println("marshal gagal:", err)
		return
	}
	fmt.Println(string(data))

	var t2 Transaksi
	sumber := []byte(`{"ID":"T-002","Waktu":"2026-01-15 09:00","Jumlah":99000,"Pelanggan":"Siti"}`)
	if err := json.Unmarshal(sumber, &t2); err != nil {
		fmt.Println("unmarshal gagal:", err)
		return
	}
	fmt.Printf("hasil decode: %+v\n", t2)

	sumber2 := []byte(`{"ID":"T-003","Waktu":"15/01/2026","Jumlah":50000,"Pelanggan":"Rina"}`)
	err = json.Unmarshal(sumber2, &t2)
	fmt.Println("galat format:", err)
}

Output yang Dihasilkan:

text
{
  "ID": "T-001",
  "Waktu": "2026-01-15 08:30",
  "Jumlah": 125050,
  "Pelanggan": "Budi"
}
hasil decode: {ID:T-002 Waktu:{Waktu:2026-01-15 09:00:00 +0000 UTC} Jumlah:{Sen:99000} Pelanggan:Siti}
galat format: WaktuBeku: format harus 2006-01-02 15:04: parsing time "15/01/2026" as "2006-01-02 15:04": cannot parse "15/01/2026" as "2006"

Mari kita telaah baris demi baris program di atas:

  1. Baris 11-19 (WaktuBeku.MarshalJSON): method dengan value receiver memformat waktu memakai layout tetap, lalu mengembalikan byte string JSON lengkap dengan tanda kutip pembuka dan penutup.
  2. Baris 22-29 (WaktuBeku.UnmarshalJSON): method dengan pointer receiver memangkas tanda kutip dari data, mengurai string menjadi time.Time, dan mengembalikan galat berbungkus %w bila format tidak cocok.
  3. Baris 32-47 (Uang): MarshalJSON menuliskan nilai sen apa adanya memakai strconv.FormatInt; UnmarshalJSON mengurai byte menjadi angka bulat dan menolak teks non-angka.
  4. Baris 49-54 (type Transaksi): struct biasa yang field Waktu dan Jumlah bertipe custom; tidak ada kode khusus di sini karena json.Marshal otomatis menemukan kedua method.
  5. Baris 57-68: json.MarshalIndent menghasilkan "Waktu": "2026-01-15 08:30" dan "Jumlah": 125050 — bentuk yang ditentukan method kita, bukan aturan bawaan.
  6. Baris 70-76: decode berhasil mengisi t2 dari string berformat sama; struct dalam memuat time.Time yang sudah terurai.
  7. Baris 78-80: input "Waktu":"15/01/2026" ditolak UnmarshalJSON dengan galat berantai yang menjelaskan format yang diharapkan.

Pointer Receiver versus Value Receiver #

UnmarshalJSON wajib memakai pointer receiver karena method tersebut harus mengubah nilai penerima; value receiver hanya mengubah salinan. MarshalJSON bebas memilih, tetapi pilihan tersebut punya konsekuensi method set: method pointer receiver tidak masuk method set nilai biasa. Akibatnya, apabila MarshalJSON dideklarasikan pada pointer receiver, pemanggilan json.Marshal(t) dengan t berupa nilai biasa tidak akan menemukan method tersebut dan diam-diam jatuh kembali ke aturan bawaan. Materi lengkap soal method set dibahas pada chapter Value Receiver dan Pointer Receiver .

Galat dari UnmarshalJSON Menjadi Galat Decode #

Galat yang dikembalikan UnmarshalJSON diteruskan apa adanya oleh json.Unmarshal, sehingga pemanggil menerima satu rantai galat yang menjelaskan field mana dan kenapa. Kebiasaan yang baik: awali pesan dengan nama tipe, sertakan contoh format yang benar, lalu bungkus galat asalnya dengan %w agar tetap bisa diperiksa errors.Is atau errors.As di hulu, sebagaimana dibahas pada chapter Validasi Data .

Referensi #

Custom marshaler mengubah bentuk keluaran satu tipe. Untuk mengatur nama field, memangkas field kosong, dan menandai format banyak encoder sekaligus, struct tag adalah alat yang lebih ringan — materi chapter Struct Tag berikutnya.

Berkas Sumber

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

marshaler-demo.go

Unduh berkas
go
package main

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

type WaktuBeku struct {
	Waktu time.Time
}

// MarshalJSON mengubah WaktuBeku menjadi string berformat "2006-01-02 15:04".
func (w WaktuBeku) MarshalJSON() ([]byte, error) {
	stempel := w.Waktu.Format("2006-01-02 15:04")
	return []byte(`"` + stempel + `"`), nil
}

// UnmarshalJSON mem-parsing string "2006-01-02 15:04" kembali menjadi waktu.
func (w *WaktuBeku) UnmarshalJSON(data []byte) error {
	t, err := time.Parse("2006-01-02 15:04", strings.Trim(string(data), `"`))
	if err != nil {
		return fmt.Errorf("WaktuBeku: format harus 2006-01-02 15:04: %w", err)
	}
	w.Waktu = t
	return nil
}

// Uang menyimpan nilai dalam satuan sen agar perhitungan tetap bulat.
type Uang struct {
	Sen int64
}

func (u Uang) MarshalJSON() ([]byte, error) {
	return []byte(strconv.FormatInt(u.Sen, 10)), nil
}

func (u *Uang) UnmarshalJSON(data []byte) error {
	n, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
	if err != nil {
		return fmt.Errorf("Uang: harus angka bulat sen: %w", err)
	}
	u.Sen = n
	return nil
}

type Transaksi struct {
	ID        string
	Waktu     WaktuBeku
	Jumlah    Uang
	Pelanggan string
}

func main() {
	t := Transaksi{
		ID:        "T-001",
		Waktu:     WaktuBeku{Waktu: time.Date(2026, 1, 15, 8, 30, 0, 0, time.UTC)},
		Jumlah:    Uang{Sen: 125050},
		Pelanggan: "Budi",
	}
	data, err := json.MarshalIndent(t, "", "  ")
	if err != nil {
		fmt.Println("marshal gagal:", err)
		return
	}
	fmt.Println(string(data))

	var t2 Transaksi
	sumber := []byte(`{"ID":"T-002","Waktu":"2026-01-15 09:00","Jumlah":99000,"Pelanggan":"Siti"}`)
	if err := json.Unmarshal(sumber, &t2); err != nil {
		fmt.Println("unmarshal gagal:", err)
		return
	}
	fmt.Printf("hasil decode: %+v\n", t2)

	sumber2 := []byte(`{"ID":"T-003","Waktu":"15/01/2026","Jumlah":50000,"Pelanggan":"Rina"}`)
	err = json.Unmarshal(sumber2, &t2)
	fmt.Println("galat format:", err)
}