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:
MarshalJSON() ([]byte, error)
UnmarshalJSON(data []byte) errorKetika 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:
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:
{
"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:
- 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.
- Baris 22-29 (WaktuBeku.UnmarshalJSON): method dengan pointer receiver memangkas tanda kutip dari
data, mengurai string menjaditime.Time, dan mengembalikan galat berbungkus%wbila format tidak cocok. - Baris 32-47 (Uang):
MarshalJSONmenuliskan nilai sen apa adanya memakaistrconv.FormatInt;UnmarshalJSONmengurai byte menjadi angka bulat dan menolak teks non-angka. - Baris 49-54 (type Transaksi): struct biasa yang field
WaktudanJumlahbertipe custom; tidak ada kode khusus di sini karenajson.Marshalotomatis menemukan kedua method. - Baris 57-68:
json.MarshalIndentmenghasilkan"Waktu": "2026-01-15 08:30"dan"Jumlah": 125050— bentuk yang ditentukan method kita, bukan aturan bawaan. - Baris 70-76: decode berhasil mengisi
t2dari string berformat sama; struct dalam memuattime.Timeyang sudah terurai. - Baris 78-80: input
"Waktu":"15/01/2026"ditolakUnmarshalJSONdengan 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 #
- Dokumentasi package encoding/json
— definisi interface
MarshalerdanUnmarshaler. - Blog Go: JSON and Go — bagian akhir memperlihatkan contoh custom marshaler resmi.
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 berkaspackage 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)
}