Encoding Json #
JSON adalah format pertukaran data yang paling umum digunakan di API modern — hampir tidak ada aplikasi Go yang berinteraksi dengan dunia luar tanpa menyentuh JSON. Package encoding/json menyediakan semua yang dibutuhkan: mengonversi struct Go ke JSON (Marshal), memuat JSON ke struct (Unmarshal), streaming JSON dari dan ke io.Reader/io.Writer, serta mekanisme kustomisasi perilaku encoding dan decoding. Yang membuat encoding/json menarik adalah pendekatannya yang berbasis refleksi — kamu mendefinisikan struct dengan tag json: dan package ini menangani semua konversi secara otomatis. Tapi ada banyak detail penting yang perlu dipahami: penanganan nil vs nilai kosong, field yang di-omit, tipe yang tidak bisa di-marshal, error yang halus saat decoding, dan kapan streaming lebih baik dari buffered marshal.
Gambaran Besar Package encoding/json #
flowchart LR
subgraph Encode["Struct → JSON"]
E1["json.Marshal(v)\nstruct → []byte"]
E2["json.MarshalIndent(v, '', ' ')\nstruct → []byte (pretty)"]
E3["json.NewEncoder(w).Encode(v)\nstruct → io.Writer (streaming)"]
end
subgraph Decode["JSON → Struct"]
D1["json.Unmarshal(data, &v)\n[]byte → struct"]
D2["json.NewDecoder(r).Decode(&v)\nio.Reader → struct (streaming)"]
end
subgraph Tags["Struct Tags"]
T1['json:"nama_field"']
T2['json:"nama,omitempty"']
T3['json:"-"']
T4['json:",string"']
end
subgraph Custom["Kustomisasi"]
C1["json.Marshaler interface\nMarshalJSON() ([]byte, error)"]
C2["json.Unmarshaler interface\nUnmarshalJSON([]byte) error"]
C3["json.RawMessage\nJSON mentah tanpa parsing"]
end
Encode --> JSON["JSON"]
JSON --> Decode
Tags --> Encode
Tags --> Decode
Custom --> Encode
Custom --> Decode
style Encode fill:#e8f5e9
style Decode fill:#e3f2fd
style Tags fill:#fff3e0
style Custom fill:#f3e5f5Marshal — Struct ke JSON #
json.Marshal mengonversi nilai Go menjadi []byte berisi JSON. Ia menggunakan refleksi untuk membaca field struct dan tag json: untuk menentukan nama field dalam output.
package main
import (
"encoding/json"
"fmt"
)
type Produk struct {
ID int `json:"id"`
Nama string `json:"nama"`
Harga float64 `json:"harga"`
Tersedia bool `json:"tersedia"`
}
func main() {
p := Produk{
ID: 1,
Nama: "Laptop Go Edition",
Harga: 15000000,
Tersedia: true,
}
// Marshal ke []byte
data, err := json.Marshal(p)
if err != nil {
fmt.Println("marshal error:", err)
return
}
fmt.Println(string(data))
// {"id":1,"nama":"Laptop Go Edition","harga":1.5e+07,"tersedia":true}
// MarshalIndent — untuk output yang mudah dibaca manusia
dataIndent, err := json.MarshalIndent(p, "", " ")
if err != nil {
fmt.Println("marshal error:", err)
return
}
fmt.Println(string(dataIndent))
// {
// "id": 1,
// "nama": "Laptop Go Edition",
// "harga": 1.5e+07,
// "tersedia": true
// }
}
Apa yang Bisa dan Tidak Bisa di-Marshal #
flowchart TD
V["Nilai Go"] --> Can{"Bisa\ndi-marshal?"}
Can -- Ya --> Y1["struct — jadi object JSON"]
Can -- Ya --> Y2["map[string]T — jadi object JSON"]
Can -- Ya --> Y3["[]T, [N]T — jadi array JSON"]
Can -- Ya --> Y4["string — jadi string JSON"]
Can -- Ya --> Y5["int, float — jadi number JSON"]
Can -- Ya --> Y6["bool — jadi true/false"]
Can -- Ya --> Y7["nil pointer — jadi null"]
Can -- Ya --> Y8["time.Time — jadi string RFC3339"]
Can -- Error --> N1["channel — tidak bisa"]
Can -- Error --> N2["func — tidak bisa"]
Can -- Error --> N3["complex — tidak bisa"]
Can -- Infinite --> N4["map dengan kunci\nnon-string non-int"]
style Y1 fill:#e8f5e9
style Y2 fill:#e8f5e9
style Y3 fill:#e8f5e9
style Y4 fill:#e8f5e9
style Y5 fill:#e8f5e9
style Y6 fill:#e8f5e9
style Y7 fill:#e8f5e9
style Y8 fill:#e8f5e9
style N1 fill:#fce4ec
style N2 fill:#fce4ec
style N3 fill:#fce4ec
style N4 fill:#fff3e0// Tipe-tipe yang bisa di-marshal
m := map[string]int{"a": 1, "b": 2}
data, _ := json.Marshal(m)
fmt.Println(string(data)) // {"a":1,"b":2}
slice := []string{"satu", "dua", "tiga"}
data, _ = json.Marshal(slice)
fmt.Println(string(data)) // ["satu","dua","tiga"]
// nil — menjadi null
var p *Produk = nil
data, _ = json.Marshal(p)
fmt.Println(string(data)) // null
// interface{} / any — marshal nilai di dalamnya
var v any = map[string]any{
"nama": "Budi",
"umur": 30,
"aktif": true,
}
data, _ = json.Marshal(v)
fmt.Println(string(data)) // {"aktif":true,"nama":"Budi","umur":30}
// Tipe yang TIDAK bisa di-marshal
ch := make(chan int)
_, err := json.Marshal(ch)
fmt.Println(err) // json: unsupported type: chan int
Struct Tags — Mengontrol Serialisasi #
Struct tag json: adalah mekanisme utama untuk mengontrol bagaimana field struct direpresentasikan dalam JSON.
type Pengguna struct {
// Nama field berbeda antara Go dan JSON
ID int `json:"id"`
NamaLengkap string `json:"nama_lengkap"`
// omitempty — hilangkan field jika nilainya zero value
Telepon string `json:"telepon,omitempty"` // hilang jika ""
Umur int `json:"umur,omitempty"` // hilang jika 0
Aktif bool `json:"aktif,omitempty"` // hilang jika false
Skor float64 `json:"skor,omitempty"` // hilang jika 0.0
Tags []string `json:"tags,omitempty"` // hilang jika nil atau []
// - (minus) — selalu hilangkan field ini dari JSON
Password string `json:"-"`
Token string `json:"-"`
// ,string — encode/decode angka sebagai string JSON
// berguna untuk JavaScript yang tidak bisa handle int64 dengan tepat
IDExternal int64 `json:"id_external,string"`
// Tanpa tag — nama field Go digunakan apa adanya
Catatan string // → "Catatan" dalam JSON
// Embedded struct — field-nya "naik" ke level atas
Alamat
}
type Alamat struct {
Kota string `json:"kota"`
Provinsi string `json:"provinsi"`
}
// Contoh output
p := Pengguna{
ID: 1,
NamaLengkap: "Budi Santoso",
Telepon: "", // omitempty — tidak muncul
Umur: 0, // omitempty — tidak muncul
IDExternal: 12345678901234,
Password: "rahasia", // json:"-" — tidak muncul
Alamat: Alamat{
Kota: "Jakarta",
Provinsi: "DKI Jakarta",
},
}
data, _ := json.MarshalIndent(p, "", " ")
fmt.Println(string(data))
// {
// "id": 1,
// "nama_lengkap": "Budi Santoso",
// "id_external": "12345678901234", ← angka sebagai string
// "Catatan": "",
// "kota": "Jakarta", ← dari embedded Alamat
// "provinsi": "DKI Jakarta"
// }
Pointer untuk Membedakan Zero Value dan Tidak Ada #
omitempty tidak bisa membedakan antara false yang sengaja diset dan false karena belum diisi. Pointer memecahkan masalah ini:
// ANTI-PATTERN: tidak bisa bedakan "aktif=false" vs "tidak diset"
type KonfigBuruk struct {
Aktif bool `json:"aktif,omitempty"`
// Jika Aktif=false, field ini hilang dari JSON
// padahal false bisa berarti "sengaja dinonaktifkan"!
}
// BENAR: gunakan pointer untuk nilai yang bisa null secara sengaja
type KonfigBaik struct {
Aktif *bool `json:"aktif,omitempty"`
// nil → hilang dari JSON (belum diset)
// &false → muncul sebagai false (sengaja dinonaktifkan)
// &true → muncul sebagai true (sengaja diaktifkan)
}
// Helper untuk membuat pointer ke literal
func boolPtr(b bool) *bool { return &b }
func intPtr(n int) *int { return &n }
cfg := KonfigBaik{
Aktif: boolPtr(false), // muncul sebagai "aktif": false
}
data, _ := json.Marshal(cfg)
fmt.Println(string(data)) // {"aktif":false}
cfg2 := KonfigBaik{
Aktif: nil, // tidak muncul sama sekali
}
data, _ = json.Marshal(cfg2)
fmt.Println(string(data)) // {}
Unmarshal — JSON ke Struct #
json.Unmarshal memuat JSON dari []byte ke struct. Field yang tidak ada di JSON dibiarkan di nilai zero-nya, dan field JSON yang tidak ada di struct diabaikan.
type Artikel struct {
ID int `json:"id"`
Judul string `json:"judul"`
Konten string `json:"konten"`
Tags []string `json:"tags"`
Diterbitkan bool `json:"diterbitkan"`
}
jsonData := []byte(`{
"id": 42,
"judul": "Belajar Go",
"konten": "Go adalah bahasa yang menyenangkan",
"tags": ["go", "programming", "tutorial"],
"diterbitkan": true,
"field_tidak_ada": "diabaikan"
}`)
var artikel Artikel
err := json.Unmarshal(jsonData, &artikel)
if err != nil {
fmt.Println("unmarshal error:", err)
return
}
fmt.Println(artikel.ID) // 42
fmt.Println(artikel.Judul) // Belajar Go
fmt.Println(artikel.Tags) // [go programming tutorial]
fmt.Println(artikel.Diterbitkan) // true
Unmarshal ke map — Struktur Dinamis #
Saat struktur JSON tidak diketahui sebelumnya, unmarshal ke map[string]any:
jsonData := []byte(`{
"nama": "Budi",
"umur": 30,
"aktif": true,
"skor": 98.5,
"tags": ["admin", "user"]
}`)
var hasil map[string]any
err := json.Unmarshal(jsonData, &hasil)
if err != nil {
fmt.Println("error:", err)
return
}
// Akses nilai dengan type assertion
nama := hasil["nama"].(string)
umur := hasil["umur"].(float64) // PERHATIAN: semua angka JSON → float64!
aktif := hasil["aktif"].(bool)
fmt.Printf("Nama: %s, Umur: %.0f, Aktif: %v\n", nama, umur, aktif)
// Iterasi semua field
for key, val := range hasil {
fmt.Printf("%s (%T): %v\n", key, val, val)
}
Saat unmarshal kemap[string]anyatauinterface{}, semua angka JSON dikonversi kefloat64, bukanint. Ini adalah perilaku default packageencoding/json. Untuk mendapatkan angka sebagaiint, gunakanjson.NumberdenganDecoder.UseNumber(), atau unmarshal ke struct dengan tipe yang tepat.
Menangani Field yang Tidak Diketahui #
// Deteksi field yang tidak dikenal dalam JSON input
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
jsonData := []byte(`{"host":"localhost","port":8080,"debug":true}`)
// DisallowUnknownFields — error jika ada field yang tidak dikenal
decoder := json.NewDecoder(strings.NewReader(string(jsonData)))
decoder.DisallowUnknownFields()
var cfg Config
if err := decoder.Decode(&cfg); err != nil {
fmt.Println(err)
// json: unknown field "debug"
}
Encoder dan Decoder — Streaming JSON #
json.NewEncoder dan json.NewDecoder bekerja langsung dengan io.Writer dan io.Reader — lebih efisien dari Marshal/Unmarshal karena tidak perlu membuffer seluruh data di memori.
flowchart LR
subgraph Buffered["Buffered (Marshal/Unmarshal)"]
B1["Struct"] --> B2["json.Marshal"] --> B3["[]byte\n(seluruh data di memori)"] --> B4["io.Writer"]
B5["io.Reader"] --> B6["baca semua\nke []byte"] --> B7["json.Unmarshal"] --> B8["Struct"]
end
subgraph Stream["Streaming (Encoder/Decoder)"]
S1["Struct"] --> S2["Encoder.Encode"] --> S3["io.Writer\n(tulis langsung)"]
S4["io.Reader"] --> S5["Decoder.Decode"] --> S6["Struct\n(baca per token)"]
end
subgraph When["Gunakan Streaming Jika"]
W1["Response HTTP — ResponseWriter adalah io.Writer"]
W2["File JSON besar — tidak muat di memori"]
W3["JSON Lines — banyak objek dalam satu stream"]
W4["Request body — Body adalah io.Reader"]
end
style Buffered fill:#fff3e0
style Stream fill:#e8f5e9
style When fill:#e3f2fdEncoder — Menulis JSON ke io.Writer #
import (
"encoding/json"
"net/http"
"os"
)
// Menulis JSON ke HTTP response (paling umum)
func handlerProduk(w http.ResponseWriter, r *http.Request) {
produk := Produk{ID: 1, Nama: "Laptop", Harga: 15000000}
w.Header().Set("Content-Type", "application/json")
// BENAR: gunakan Encoder langsung ke ResponseWriter
if err := json.NewEncoder(w).Encode(produk); err != nil {
// Jika error di sini, header sudah terkirim — tidak bisa ubah status code
// Log saja
fmt.Fprintf(os.Stderr, "encode error: %v\n", err)
}
}
// Menulis JSON ke file
func simpanKeFile(path string, data any) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("simpanKeFile: %w", err)
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ") // pretty print
if err := encoder.Encode(data); err != nil {
return fmt.Errorf("simpanKeFile encode: %w", err)
}
return nil
}
// Menulis banyak objek sebagai JSON Lines (NDJSON)
// Format: satu JSON object per baris, berguna untuk log dan stream
func tulisJSONLines(w io.Writer, items []Produk) error {
encoder := json.NewEncoder(w)
for _, item := range items {
if err := encoder.Encode(item); err != nil {
return fmt.Errorf("tulisJSONLines: %w", err)
}
// Encode otomatis menambahkan newline setelah setiap objek
}
return nil
}
Decoder — Membaca JSON dari io.Reader #
// Membaca JSON dari HTTP request body
func handlerBuatProduk(w http.ResponseWriter, r *http.Request) {
var produk Produk
// BENAR: gunakan Decoder langsung dari Body
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // opsional: reject field yang tidak dikenal
if err := decoder.Decode(&produk); err != nil {
http.Error(w, "request body tidak valid: "+err.Error(),
http.StatusBadRequest)
return
}
// Validasi setelah decode
if produk.Nama == "" {
http.Error(w, "nama produk tidak boleh kosong", http.StatusBadRequest)
return
}
// Proses produk...
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(produk)
}
// Membaca JSON Lines dari file — satu objek per baris
func bacaJSONLines(path string) ([]Produk, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("bacaJSONLines: %w", err)
}
defer f.Close()
var produk []Produk
decoder := json.NewDecoder(f)
for decoder.More() { // More() mengembalikan true jika masih ada data
var p Produk
if err := decoder.Decode(&p); err != nil {
return nil, fmt.Errorf("bacaJSONLines decode: %w", err)
}
produk = append(produk, p)
}
return produk, nil
}
json.RawMessage — JSON Mentah #
json.RawMessage adalah []byte yang mengimplementasikan json.Marshaler dan json.Unmarshaler — berguna untuk menunda parsing sebagian JSON atau meneruskan JSON tanpa mengubahnya.
// Pola 1: field yang isinya JSON bebas (schema tidak diketahui)
type EventLog struct {
TipeEvent string `json:"tipe"`
Waktu time.Time `json:"waktu"`
Data json.RawMessage `json:"data"` // JSON apapun
}
// Marshal: Data tetap sebagai JSON apa adanya
log := EventLog{
TipeEvent: "pembelian",
Waktu: time.Now(),
Data: json.RawMessage(`{"produk_id":42,"jumlah":2,"total":30000}`),
}
data, _ := json.Marshal(log)
fmt.Println(string(data))
// {"tipe":"pembelian","waktu":"...","data":{"produk_id":42,"jumlah":2,"total":30000}}
// Unmarshal: Data tidak di-parse, disimpan apa adanya
var logBaca EventLog
json.Unmarshal(data, &logBaca)
fmt.Println(string(logBaca.Data))
// {"produk_id":42,"jumlah":2,"total":30000}
// Pola 2: discriminated union — parse berdasarkan field tipe
type Pesan struct {
Tipe string `json:"tipe"`
Payload json.RawMessage `json:"payload"`
}
type PayloadChat struct {
Teks string `json:"teks"`
Pengirim string `json:"pengirim"`
}
type PayloadGambar struct {
URL string `json:"url"`
Ukuran int `json:"ukuran"`
}
func parsePesan(data []byte) (any, error) {
var pesan Pesan
if err := json.Unmarshal(data, &pesan); err != nil {
return nil, err
}
switch pesan.Tipe {
case "chat":
var p PayloadChat
if err := json.Unmarshal(pesan.Payload, &p); err != nil {
return nil, err
}
return p, nil
case "gambar":
var p PayloadGambar
if err := json.Unmarshal(pesan.Payload, &p); err != nil {
return nil, err
}
return p, nil
default:
return nil, fmt.Errorf("tipe pesan tidak dikenal: %s", pesan.Tipe)
}
}
Custom Marshaler dan Unmarshaler #
Untuk mengontrol sepenuhnya bagaimana sebuah tipe dikonversi ke dan dari JSON, implementasikan interface json.Marshaler dan json.Unmarshaler.
sequenceDiagram
participant App as Kode Aplikasi
participant Enc as json.Marshal
participant Type as Tipe dengan MarshalJSON
App->>Enc: json.Marshal(nilai)
Enc->>Enc: Periksa apakah tipe\nimplementasi json.Marshaler
Enc->>Type: MarshalJSON()
Type-->>Enc: []byte JSON kustom
Enc-->>App: JSON final
Note over Enc,Type: Sama untuk Unmarshal → UnmarshalJSON([]byte)// Contoh 1: time.Time dengan format kustom (bukan RFC3339 default)
type Tanggal struct {
time.Time
}
func (t Tanggal) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Format("2006-01-02"))
}
func (t *Tanggal) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsed, err := time.Parse("2006-01-02", s)
if err != nil {
return fmt.Errorf("Tanggal.UnmarshalJSON: format harus YYYY-MM-DD: %w", err)
}
t.Time = parsed
return nil
}
type Pesanan struct {
ID int `json:"id"`
TanggalBuat Tanggal `json:"tanggal_buat"`
Total float64 `json:"total"`
}
// Output: {"id":1,"tanggal_buat":"2024-03-15","total":150000}
// bukan: {"id":1,"tanggal_buat":"2024-03-15T00:00:00Z","total":150000}
// Contoh 2: enum / konstanta sebagai string
type StatusPesanan int
const (
StatusMenunggu StatusPesanan = iota
StatusDiproses
StatusDikirim
StatusSelesai
StatusDibatalkan
)
var statusNama = map[StatusPesanan]string{
StatusMenunggu: "menunggu",
StatusDiproses: "diproses",
StatusDikirim: "dikirim",
StatusSelesai: "selesai",
StatusDibatalkan: "dibatalkan",
}
var namaStatus = map[string]StatusPesanan{
"menunggu": StatusMenunggu,
"diproses": StatusDiproses,
"dikirim": StatusDikirim,
"selesai": StatusSelesai,
"dibatalkan": StatusDibatalkan,
}
func (s StatusPesanan) MarshalJSON() ([]byte, error) {
nama, ok := statusNama[s]
if !ok {
return nil, fmt.Errorf("StatusPesanan.MarshalJSON: nilai tidak valid: %d", s)
}
return json.Marshal(nama)
}
func (s *StatusPesanan) UnmarshalJSON(data []byte) error {
var nama string
if err := json.Unmarshal(data, &nama); err != nil {
return err
}
status, ok := namaStatus[nama]
if !ok {
return fmt.Errorf("StatusPesanan.UnmarshalJSON: nilai tidak valid: %q", nama)
}
*s = status
return nil
}
type Pesanan2 struct {
ID int `json:"id"`
Status StatusPesanan `json:"status"`
}
p := Pesanan2{ID: 1, Status: StatusDikirim}
data, _ := json.Marshal(p)
fmt.Println(string(data)) // {"id":1,"status":"dikirim"}
Menangani Error JSON dengan Tepat #
Error dari json.Unmarshal bisa bermacam-macam jenisnya — memahaminya membantu memberikan pesan error yang lebih informatif ke pengguna.
import (
"encoding/json"
"errors"
)
func decodeRequest(body io.Reader, target any) error {
decoder := json.NewDecoder(body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
var syntaxErr *json.SyntaxError
var typeErr *json.UnmarshalTypeError
switch {
case errors.As(err, &syntaxErr):
return fmt.Errorf("JSON tidak valid pada posisi %d: %w",
syntaxErr.Offset, err)
case errors.As(err, &typeErr):
return fmt.Errorf("tipe tidak cocok: field %q mengharapkan %v, dapat %v",
typeErr.Field, typeErr.Type, typeErr.Value)
case errors.Is(err, io.EOF):
return fmt.Errorf("request body kosong")
case errors.Is(err, io.ErrUnexpectedEOF):
return fmt.Errorf("JSON tidak lengkap")
default:
return fmt.Errorf("gagal decode request: %w", err)
}
}
return nil
}
// Penggunaan di handler
func handlerBuatPengguna(w http.ResponseWriter, r *http.Request) {
var pengguna Pengguna
if err := decodeRequest(r.Body, &pengguna); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// lanjutkan...
}
Pola Penggunaan di Produksi #
Response API yang Konsisten #
// Struktur response API yang seragam
type APIResponse struct {
Sukses bool `json:"sukses"`
Data any `json:"data,omitempty"`
Pesan string `json:"pesan,omitempty"`
Error string `json:"error,omitempty"`
}
func kirimJSON(w http.ResponseWriter, statusCode int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(data); err != nil {
fmt.Fprintf(os.Stderr, "kirimJSON encode error: %v\n", err)
}
}
func kirimSukses(w http.ResponseWriter, data any) {
kirimJSON(w, http.StatusOK, APIResponse{
Sukses: true,
Data: data,
})
}
func kirimError(w http.ResponseWriter, statusCode int, pesan string) {
kirimJSON(w, statusCode, APIResponse{
Sukses: false,
Error: pesan,
})
}
// Penggunaan di handler
func handlerDaftarProduk(w http.ResponseWriter, r *http.Request) {
produk, err := serviceDaftarProduk()
if err != nil {
kirimError(w, http.StatusInternalServerError, "gagal memuat produk")
return
}
kirimSukses(w, produk)
}
Konfigurasi dari File JSON #
type AppConfig struct {
Server ServerConfig `json:"server"`
Database DatabaseConfig `json:"database"`
Log LogConfig `json:"log"`
}
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
ReadTimeout time.Duration `json:"read_timeout"`
WriteTimeout time.Duration `json:"write_timeout"`
}
type DatabaseConfig struct {
DSN string `json:"dsn"`
MaxOpenConn int `json:"max_open_conn"`
MaxIdleConn int `json:"max_idle_conn"`
}
type LogConfig struct {
Level string `json:"level"`
Format string `json:"format"`
}
func muatKonfigurasi(path string) (*AppConfig, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("muatKonfigurasi: %w", err)
}
defer f.Close()
// Nilai default sebelum decode
cfg := &AppConfig{
Server: ServerConfig{
Host: "0.0.0.0",
Port: 8080,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
},
Log: LogConfig{
Level: "info",
Format: "json",
},
}
decoder := json.NewDecoder(f)
decoder.DisallowUnknownFields()
if err := decoder.Decode(cfg); err != nil {
return nil, fmt.Errorf("muatKonfigurasi decode: %w", err)
}
return cfg, nil
}
Transformasi JSON tanpa Struct #
// Ubah field nama dalam JSON tanpa struct lengkap
func gantiFiieldJSON(input []byte, lama, baru string) ([]byte, error) {
var data map[string]json.RawMessage
if err := json.Unmarshal(input, &data); err != nil {
return nil, err
}
if val, ada := data[lama]; ada {
data[baru] = val
delete(data, lama)
}
return json.Marshal(data)
}
// Filter field sensitif sebelum dikirim ke client
type PenggunaPublik struct {
ID int `json:"id"`
Nama string `json:"nama"`
// Password, Token, dll tidak ada di sini
}
func filterPengguna(p *Pengguna) PenggunaPublik {
return PenggunaPublik{ID: p.ID, Nama: p.NamaLengkap}
}
Streaming Banyak Objek #
// Export ribuan produk ke JSON Lines tanpa membuffer semua di memori
func eksportProduk(w io.Writer, repo ProdukRepository) error {
encoder := json.NewEncoder(w)
// Iterasi dari database dengan cursor, bukan load semua sekaligus
cursor, err := repo.Cursor()
if err != nil {
return fmt.Errorf("eksportProduk: buka cursor: %w", err)
}
defer cursor.Close()
jumlah := 0
for cursor.Next() {
produk, err := cursor.Scan()
if err != nil {
return fmt.Errorf("eksportProduk: scan baris %d: %w", jumlah, err)
}
if err := encoder.Encode(produk); err != nil {
return fmt.Errorf("eksportProduk: encode baris %d: %w", jumlah, err)
}
jumlah++
}
fmt.Fprintf(os.Stderr, "Ekspor selesai: %d produk\n", jumlah)
return cursor.Err()
}
Kapan Beralih ke Alternatif #
Tetap gunakan encoding/json jika:
✓ Marshal dan unmarshal struct ke/dari JSON
✓ Streaming JSON ke HTTP response atau dari request body
✓ Konfigurasi sederhana dari file JSON
✓ API internal antar service Go
Pertimbangkan encoding/json dengan json.Number jika:
✗ Unmarshal angka besar yang tidak bisa direpresentasikan oleh float64
✗ Butuh tahu tipe angka asli (int vs float) dari JSON yang tidak terstruktur
Pertimbangkan library eksternal jika:
✗ Performa sangat kritikal (ribuan marshal per detik) → sonic, go-json, jsoniter
(bisa 3-10× lebih cepat dari encoding/json untuk kasus tertentu)
✗ JSON Schema validation → gojsonschema atau library validator
✗ JSONPath query → gjson untuk akses field tanpa unmarshal penuh
✗ JSON Patch / JSON Merge Patch → RFC 6902 implementation
✗ YAML dengan konversi ke JSON → gopkg.in/yaml.v3
Ringkasan #
- Struct tag
json:"nama"mengontrol nama field dalam JSON — gunakan snake_case untuk kompatibilitas dengan konvensi JSON yang umum.omitemptymenghilangkan field jika nilainya zero value ("",0,false,nil) — gunakan pointer (*bool,*int) jika perlu membedakan “tidak diset” dari “diset ke zero”.json:"-"selalu menghilangkan field dari JSON — gunakan untuk Password, Token, dan field sensitif lainnya.- Semua angka JSON menjadi
float64saat unmarshal keinterface{}ataumap[string]any— gunakandecoder.UseNumber()atau unmarshal ke struct dengan tipe yang tepat jika presisi penting.- Gunakan
json.NewEncoder(w).Encode(v)bukanjson.Marshaluntuk HTTP response — lebih efisien karena tidak membuffer semua data di memori.- Gunakan
json.NewDecoder(r.Body).Decode(&v)bukanjson.Unmarshaluntuk request body — streaming langsung dari body tanpa membaca semua ke[]byteterlebih dahulu.json.RawMessagememungkinkan menyimpan dan meneruskan JSON mentah tanpa parsing — berguna untuk discriminated union atau field dengan schema dinamis.- Custom
MarshalJSON/UnmarshalJSONmemberikan kontrol penuh atas format — gunakan untuk enum, format tanggal kustom, atau representasi yang tidak bisa diwakili oleh tag saja.decoder.DisallowUnknownFields()membantu mendeteksi typo atau field yang tidak didukung di request body — berguna untuk API yang ketat.errors.Asdengan*json.SyntaxErrordan*json.UnmarshalTypeErrormemungkinkan pesan error yang spesifik tentang apa yang salah dalam JSON input.