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:#f3e5f5

Marshal — 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 ke map[string]any atau interface{}, semua angka JSON dikonversi ke float64, bukan int. Ini adalah perilaku default package encoding/json. Untuk mendapatkan angka sebagai int, gunakan json.Number dengan Decoder.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:#e3f2fd

Encoder — 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.
  • omitempty menghilangkan 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 float64 saat unmarshal ke interface{} atau map[string]any — gunakan decoder.UseNumber() atau unmarshal ke struct dengan tipe yang tepat jika presisi penting.
  • Gunakan json.NewEncoder(w).Encode(v) bukan json.Marshal untuk HTTP response — lebih efisien karena tidak membuffer semua data di memori.
  • Gunakan json.NewDecoder(r.Body).Decode(&v) bukan json.Unmarshal untuk request body — streaming langsung dari body tanpa membaca semua ke []byte terlebih dahulu.
  • json.RawMessage memungkinkan menyimpan dan meneruskan JSON mentah tanpa parsing — berguna untuk discriminated union atau field dengan schema dinamis.
  • Custom MarshalJSON/UnmarshalJSON memberikan 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.As dengan *json.SyntaxError dan *json.UnmarshalTypeError memungkinkan pesan error yang spesifik tentang apa yang salah dalam JSON input.

← Sebelumnya: Errors   Berikutnya: Net Http →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact