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,omitemptyuntuk skip zero value,"-"untuk selalu skip,stringuntuk 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.Decoderlebih 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.RawMessageuntuk 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/UnmarshalJSONuntuk tipe kustom (tanggal dengan format tertentu, enum sebagai string, Money).map[string]interface{}untuk JSON dinamis — tapi ingat JSON number selalufloat64, bukanint.json.MarshalIndentuntuk output yang mudah dibaca manusia (debugging, log, config file).json.SetEscapeHTML(false)pada encoder untuk mencegah<,>,&di-escape menjadi\u003c, dll.