JSON #

JSON (JavaScript Object Notation) adalah format pertukaran data yang paling umum digunakan di web API modern. Go menyediakan package encoding/json di standard library yang sangat capable untuk encode (marshal) dan decode (unmarshal) JSON. Package ini bekerja secara reflektif dengan struct Go, menjadikan konversi antara JSON dan tipe Go sangat mulus — dengan beberapa nuansa penting yang perlu dipahami.

Marshal — Go ke JSON #

json.Marshal mengkonversi nilai Go ke representasi JSON dalam []byte:

import "encoding/json"

type Product struct {
    ID       int     `json:"id"`
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    InStock  bool    `json:"in_stock"`
}

p := Product{ID: 1, Name: "Laptop Pro", Price: 15_000_000, InStock: true}

data, err := json.Marshal(p)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))
// {"id":1,"name":"Laptop Pro","price":1.5e+07,"in_stock":true}

// Pretty print dengan indentasi
pretty, _ := json.MarshalIndent(p, "", "  ")
fmt.Println(string(pretty))
// {
//   "id": 1,
//   "name": "Laptop Pro",
//   "price": 1.5e+07,
//   "in_stock": true
// }

Struct Tags JSON #

Struct tags mengontrol bagaimana setiap field diperlakukan saat marshal/unmarshal:

type User struct {
    // Tag dasar: ganti nama field di JSON
    ID        int    `json:"id"`
    FirstName string `json:"first_name"`

    // omitempty: field ini diabaikan jika zero value (0, "", false, nil, [], {})
    MiddleName string `json:"middle_name,omitempty"`
    Age        int    `json:"age,omitempty"`

    // "-": field ini selalu diabaikan (tidak masuk JSON sama sekali)
    Password   string `json:"-"`
    // "-,": field ini punya nama "-" di JSON (edge case)
    Dash       string `json:"-,"`

    // string: encode nilai numerik/bool sebagai JSON string
    Score      float64 `json:"score,string"`

    // Tanpa tag: nama field digunakan apa adanya
    Status string  // → "Status" di JSON

    // Unexported field TIDAK pernah di-marshal (diabaikan)
    secret string
}

u := User{
    ID: 1, FirstName: "Budi",
    Password: "rahasia",  // tidak akan masuk JSON
    Score: 9.5,
    Status: "active",
}
data, _ := json.Marshal(u)
// {"id":1,"first_name":"Budi","score":"9.5","Status":"active"}
// MiddleName dan Age tidak muncul (omitempty, zero value)
// Password tidak muncul (tag "-")

Unmarshal — JSON ke Go #

json.Unmarshal mengkonversi []byte JSON ke struct Go:

jsonStr := `{
    "id": 42,
    "name": "Sari",
    "email": "[email protected]",
    "age": 28
}`

type Person struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

var p Person
if err := json.Unmarshal([]byte(jsonStr), &p); err != nil {
    log.Fatal("unmarshal gagal:", err)
}
fmt.Printf("%+v\n", p)
// {ID:42 Name:Sari Email:[email protected] Age:28}

Perilaku Penting Unmarshal #

// Field JSON yang tidak ada di struct → diabaikan (tidak error)
// Field struct yang tidak ada di JSON → tetap zero value (tidak error)

// JSON number ke berbagai tipe Go
type Example struct {
    IntField   int     `json:"int"`
    FloatField float64 `json:"float"`
    // Hati-hati: JSON number besar bisa overflow int
}

// Pointer field — nil jika JSON null atau field tidak ada
type WithPointer struct {
    Name  *string `json:"name"`  // nil jika field tidak ada
    Score *int    `json:"score"` // nil jika "score": null
}

// Nested struct
type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
}
type UserWithAddr struct {
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

json.Unmarshal([]byte(`{
    "name": "Budi",
    "address": {"street": "Jl. Merdeka", "city": "Jakarta"}
}`), &UserWithAddr{})

Streaming dengan Encoder dan Decoder #

Untuk JSON besar atau streaming I/O, gunakan json.Encoder dan json.Decoder yang bekerja langsung dengan io.Writer dan io.Reader tanpa muat semua data ke memori:

// Encoder — tulis JSON ke writer
func writeJSONResponse(w http.ResponseWriter, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    enc := json.NewEncoder(w)
    enc.SetIndent("", "  ")          // opsional: pretty print
    enc.SetEscapeHTML(false)         // nonaktifkan HTML escaping (<, >, &)
    if err := enc.Encode(data); err != nil {
        log.Println("encode error:", err)
    }
}

// Decoder — baca JSON dari reader
func readJSONBody(r *http.Request, dest interface{}) error {
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()  // error jika ada field asing

    if err := dec.Decode(dest); err != nil {
        return fmt.Errorf("decode JSON: %w", err)
    }
    return nil
}

// Streaming array besar — decode satu elemen pada satu waktu
func processLargeJSONArray(r io.Reader) error {
    dec := json.NewDecoder(r)

    // Baca token '[' awal
    if _, err := dec.Token(); err != nil {
        return err
    }

    // Baca elemen satu per satu
    for dec.More() {
        var item Product
        if err := dec.Decode(&item); err != nil {
            return err
        }
        process(item)  // proses tanpa muat semua ke memori
    }

    // Baca token ']' akhir
    if _, err := dec.Token(); err != nil {
        return err
    }
    return nil
}

json.RawMessage — Lazy Parsing #

json.RawMessage adalah []byte yang tidak di-parse saat unmarshal induk, berguna untuk konten yang tipenya baru diketahui setelah membaca field lain:

type Event struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"`  // defer parsing
}

jsonData := `{
    "type": "user_created",
    "payload": {"id": 42, "name": "Budi"}
}`

var event Event
json.Unmarshal([]byte(jsonData), &event)

// Sekarang parse payload berdasarkan type
switch event.Type {
case "user_created":
    var user User
    json.Unmarshal(event.Payload, &user)
    fmt.Println("User dibuat:", user.Name)
case "order_placed":
    var order Order
    json.Unmarshal(event.Payload, &order)
}

Dynamic JSON dengan map dan interface{} #

Untuk JSON dengan struktur yang tidak diketahui:

// Parse JSON ke map — fleksibel tapi perlu type assertion
var result map[string]interface{}
json.Unmarshal([]byte(`{"name":"Budi","age":28,"tags":["go","dev"]}`), &result)

name := result["name"].(string)
age := result["age"].(float64)  // JSON number selalu float64 di interface{}
ageInt := int(age)
tags := result["tags"].([]interface{})
_ = name; _ = ageInt; _ = tags

// Lebih aman dengan type switch
for key, val := range result {
    switch v := val.(type) {
    case string:
        fmt.Printf("%s: string = %s\n", key, v)
    case float64:
        fmt.Printf("%s: number = %v\n", key, v)
    case bool:
        fmt.Printf("%s: bool = %v\n", key, v)
    case []interface{}:
        fmt.Printf("%s: array dengan %d elemen\n", key, len(v))
    case map[string]interface{}:
        fmt.Printf("%s: object\n", key)
    case nil:
        fmt.Printf("%s: null\n", key)
    }
}

// Gunakan json.Number untuk presisi numerik yang lebih baik
dec := json.NewDecoder(strings.NewReader(`{"id": 9999999999999999}`))
dec.UseNumber()  // parse number sebagai json.Number, bukan float64

var data map[string]interface{}
dec.Decode(&data)
id, _ := data["id"].(json.Number).Int64()  // bukan float64!
fmt.Println(id)  // 9999999999999999 — presisi terjaga

Custom Marshaler dan Unmarshaler #

Implementasikan json.Marshaler atau json.Unmarshaler untuk mengontrol encoding/decoding sepenuhnya:

// Tipe tanggal kustom dengan format tertentu
type Date struct {
    time.Time
}

func (d Date) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.Format("2006-01-02"))
}

func (d *Date) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }
    t, err := time.Parse("2006-01-02", s)
    if err != nil {
        return fmt.Errorf("format tanggal tidak valid %q: %w", s, err)
    }
    d.Time = t
    return nil
}

type Event struct {
    Name string `json:"name"`
    Date Date   `json:"date"`
}

e := Event{Name: "HUT RI", Date: Date{time.Date(2024, 8, 17, 0, 0, 0, 0, time.UTC)}}
data, _ := json.Marshal(e)
// {"name":"HUT RI","date":"2024-08-17"}

var e2 Event
json.Unmarshal([]byte(`{"name":"HUT RI","date":"2024-08-17"}`), &e2)
fmt.Println(e2.Date.Year())  // 2024

// Enum sebagai string
type Status int

const (
    StatusActive Status = iota
    StatusInactive
    StatusBanned
)

var statusNames = map[Status]string{
    StatusActive:   "active",
    StatusInactive: "inactive",
    StatusBanned:   "banned",
}

var statusValues = map[string]Status{
    "active":   StatusActive,
    "inactive": StatusInactive,
    "banned":   StatusBanned,
}

func (s Status) MarshalJSON() ([]byte, error) {
    name, ok := statusNames[s]
    if !ok {
        return nil, fmt.Errorf("status tidak dikenal: %d", s)
    }
    return json.Marshal(name)
}

func (s *Status) UnmarshalJSON(data []byte) error {
    var str string
    if err := json.Unmarshal(data, &str); err != nil {
        return err
    }
    val, ok := statusValues[str]
    if !ok {
        return fmt.Errorf("status tidak valid: %q", str)
    }
    *s = val
    return nil
}

Pola API Response #

Pattern untuk API response yang konsisten:

// Wrapper response standar
type Response[T any] struct {
    Success bool   `json:"success"`
    Data    T      `json:"data,omitempty"`
    Error   string `json:"error,omitempty"`
    Meta    *Meta  `json:"meta,omitempty"`
}

type Meta struct {
    Page       int `json:"page"`
    PerPage    int `json:"per_page"`
    Total      int `json:"total"`
    TotalPages int `json:"total_pages"`
}

func WriteSuccess[T any](w http.ResponseWriter, status int, data T) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(Response[T]{Success: true, Data: data})
}

func WriteError(w http.ResponseWriter, status int, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(Response[any]{Success: false, Error: msg})
}

// Penggunaan
func getProductHandler(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    product, err := productRepo.FindByID(id)
    if err != nil {
        WriteError(w, http.StatusNotFound, "produk tidak ditemukan")
        return
    }
    WriteSuccess(w, http.StatusOK, product)
}

Contoh Program Lengkap #

package main

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

// ── Types dengan custom JSON ───────────────────────────────────

type Money struct {
    Amount   int64  // dalam sen (Rp 1 = 100 sen)
    Currency string
}

func (m Money) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        Amount   string `json:"amount"`
        Currency string `json:"currency"`
        Display  string `json:"display"`
    }{
        Amount:   fmt.Sprintf("%.2f", float64(m.Amount)/100),
        Currency: m.Currency,
        Display:  fmt.Sprintf("Rp %s", formatIDR(m.Amount)),
    })
}

func (m *Money) UnmarshalJSON(data []byte) error {
    var raw struct {
        Amount   float64 `json:"amount"`
        Currency string  `json:"currency"`
    }
    if err := json.Unmarshal(data, &raw); err != nil {
        return err
    }
    m.Amount = int64(raw.Amount * 100)
    m.Currency = raw.Currency
    return nil
}

func formatIDR(sen int64) string {
    rupiah := sen / 100
    s := fmt.Sprintf("%d", rupiah)
    var result strings.Builder
    n := len(s)
    for i, c := range s {
        if i > 0 && (n-i)%3 == 0 {
            result.WriteByte('.')
        }
        result.WriteRune(c)
    }
    return result.String()
}

type OrderStatus string

const (
    OrderPending    OrderStatus = "pending"
    OrderProcessing OrderStatus = "processing"
    OrderShipped    OrderStatus = "shipped"
    OrderDelivered  OrderStatus = "delivered"
    OrderCancelled  OrderStatus = "cancelled"
)

type Order struct {
    ID        string      `json:"id"`
    CreatedAt time.Time   `json:"created_at"`
    Status    OrderStatus `json:"status"`
    Total     Money       `json:"total"`
    Items     []OrderItem `json:"items"`
    Note      string      `json:"note,omitempty"`
    Metadata  json.RawMessage `json:"metadata,omitempty"`
}

type OrderItem struct {
    ProductID   int     `json:"product_id"`
    ProductName string  `json:"product_name"`
    Qty         int     `json:"qty"`
    UnitPrice   Money   `json:"unit_price"`
}

func main() {
    // Marshal: Order ke JSON
    order := Order{
        ID:        "ORD-2024-001",
        CreatedAt: time.Date(2024, 7, 28, 10, 30, 0, 0, time.UTC),
        Status:    OrderProcessing,
        Total:     Money{Amount: 3_150_000_00, Currency: "IDR"},
        Items: []OrderItem{
            {
                ProductID:   1,
                ProductName: "Laptop Pro 14",
                Qty:         1,
                UnitPrice:   Money{Amount: 15_000_000_00, Currency: "IDR"},
            },
            {
                ProductID:   2,
                ProductName: "Mouse Wireless",
                Qty:         2,
                UnitPrice:   Money{Amount: 350_000_00, Currency: "IDR"},
            },
        },
        Note:     "Tolong dibungkus rapi",
        Metadata: json.RawMessage(`{"source":"web","campaign":"summer_sale"}`),
    }

    data, err := json.MarshalIndent(order, "", "  ")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("=== Marshal Output ===")
    fmt.Println(string(data))

    // Unmarshal: JSON ke Order
    jsonInput := `{
        "id": "ORD-2024-002",
        "created_at": "2024-07-28T11:00:00Z",
        "status": "pending",
        "total": {"amount": 500000, "currency": "IDR"},
        "items": [
            {
                "product_id": 3,
                "product_name": "Keyboard Mechanical",
                "qty": 1,
                "unit_price": {"amount": 500000, "currency": "IDR"}
            }
        ]
    }`

    var order2 Order
    if err := json.Unmarshal([]byte(jsonInput), &order2); err != nil {
        log.Fatal("Unmarshal gagal:", err)
    }

    fmt.Println("\n=== Unmarshal Result ===")
    fmt.Printf("ID: %s\n", order2.ID)
    fmt.Printf("Status: %s\n", order2.Status)
    fmt.Printf("Total: Rp %s\n", formatIDR(order2.Total.Amount))
    fmt.Printf("Items: %d item\n", len(order2.Items))

    // Streaming: decode beberapa JSON objects dari stream
    fmt.Println("\n=== Streaming Decode ===")
    stream := `{"id":"A","status":"pending"}
{"id":"B","status":"shipped"}
{"id":"C","status":"delivered"}`

    dec := json.NewDecoder(strings.NewReader(stream))
    for dec.More() {
        var o struct {
            ID     string `json:"id"`
            Status string `json:"status"`
        }
        if err := dec.Decode(&o); err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Order %s: %s\n", o.ID, o.Status)
    }

    // json.RawMessage untuk dynamic payload
    fmt.Println("\n=== RawMessage / Dynamic JSON ===")
    events := []struct {
        Type    string          `json:"type"`
        Payload json.RawMessage `json:"payload"`
    }{}

    eventJSON := `[
        {"type":"order","payload":{"id":"ORD-001","amount":100000}},
        {"type":"user","payload":{"id":42,"name":"Budi"}},
        {"type":"notification","payload":{"message":"Selamat!"}}
    ]`

    json.Unmarshal([]byte(eventJSON), &events)
    for _, e := range events {
        fmt.Printf("Type: %-15s | Payload: %s\n", e.Type, string(e.Payload))
    }
}

Ringkasan #

  • Struct tags json:"name" untuk rename, omitempty untuk skip zero value, "-" untuk selalu skip, string untuk encode number sebagai string.
  • Unexported fields (huruf kecil) tidak pernah di-marshal — selalu gunakan exported fields untuk struct yang perlu di-JSON-kan.
  • json.Encoder / json.Decoder lebih efisien untuk HTTP handler dan file besar karena tidak muat seluruh data ke memori.
  • dec.DisallowUnknownFields() untuk validasi ketat — error jika JSON mengandung field yang tidak ada di struct.
  • json.RawMessage untuk lazy parsing — parse konten berdasarkan kondisi (type field, version field, dll).
  • dec.UseNumber() untuk presisi numerik besar — hindari float64 yang kehilangan presisi untuk integer besar.
  • Custom MarshalJSON/UnmarshalJSON untuk tipe kustom (tanggal dengan format tertentu, enum sebagai string, Money).
  • map[string]interface{} untuk JSON dinamis — tapi ingat JSON number selalu float64, bukan int.
  • json.MarshalIndent untuk output yang mudah dibaca manusia (debugging, log, config file).
  • json.SetEscapeHTML(false) pada encoder untuk mencegah <, >, & di-escape menjadi \u003c, dll.

← Sebelumnya: Mocking   Berikutnya: Yaml →

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