JSON #
JSON (JavaScript Object Notation) adalah format pertukaran data yang paling umum digunakan dalam komunikasi antar sistem modern, terutama pada arsitektur REST, microservices, dan komunikasi client-server.
Di Golang, JSON menjadi bagian fundamental dalam pengembangan backend karena:
- Hampir semua API menggunakan JSON sebagai format request/response
- Integrasi dengan frontend (React, Vue, Mobile App) menggunakan JSON
- Komunikasi antar service (internal/external)
- Penyimpanan semi-structured data
Artikel ini akan membahas:
- Cara kerja JSON di Go
- Encoding & decoding secara mendalam
- Struct tags dan pengaruhnya
- Custom marshal & unmarshal
- Streaming JSON
- Validasi dan error handling
- Performance consideration
- Best practice produksi
Package encoding/json
#
Go menyediakan package standar:
encoding/json
Package ini menggunakan reflection untuk membaca struct dan mengubahnya menjadi JSON atau sebaliknya.
Dua fungsi utama:
json.Marshal()→ struct ke JSONjson.Unmarshal()→ JSON ke struct
Encoding (Marshal) JSON #
Contoh Dasar #
type User struct {
ID int
Name string
}
u := User{ID: 1, Name: "Unis"}
b, err := json.Marshal(u)
Output:
{"ID":1,"Name":"Unis"}
MarshalIndent (Pretty Print) #
b, err := json.MarshalIndent(u, "", " ")
Digunakan untuk logging atau debugging.
Decoding (Unmarshal) JSON #
var u User
err := json.Unmarshal(data, &u)
⚠ Penting: Harus pointer.
Kenapa? Karena Unmarshal akan mengisi nilai ke memori yang sudah dialokasikan.
Struct Tags #
Struct tag menentukan bagaimana field dipetakan.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
Opsi Struct Tag #
1. Rename Field #
`json:"user_id"`
2. Omit Empty #
`json:"name,omitempty"`
Field tidak akan muncul jika:
- string kosong
- 0
- false
- nil pointer
- slice/map kosong
3. Ignore Field #
`json:"-"`
Zero Value vs Nil Problem #
Masalah umum dalam API design:
Bagaimana membedakan:
- field tidak dikirim
- field dikirim dengan nilai default (0, false)
Solusi: gunakan pointer.
type UpdateUserRequest struct {
Name *string `json:"name"`
Age *int `json:"age"`
}
Jika nil → tidak dikirim Jika non-nil → dikirim
Ini penting untuk PATCH endpoint.
Custom Marshal & Unmarshal #
Jika butuh kontrol penuh.
Custom Marshal #
func (u User) MarshalJSON() ([]byte, error) {
type Alias User
return json.Marshal(&struct {
Alias
Name string `json:"name"`
}{
Alias: Alias(u),
Name: strings.ToUpper(u.Name),
})
}
Custom Unmarshal #
func (u *User) UnmarshalJSON(data []byte) error {
type Alias User
aux := &struct {
*Alias
}{
Alias: (*Alias)(u),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
u.Name = strings.TrimSpace(u.Name)
return nil
}
Digunakan untuk:
- Format tanggal khusus
- Enum validation
- Transformasi nilai
- Backward compatibility
Handling JSON di HTTP Server #
Contoh sederhana:
func handler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Kenapa NewDecoder dan bukan Unmarshal?
Karena lebih memory efficient dan langsung membaca dari stream.
Streaming JSON #
Jika menangani payload besar.
decoder := json.NewDecoder(r.Body)
for decoder.More() {
var item Item
if err := decoder.Decode(&item); err != nil {
return err
}
}
Digunakan untuk:
- Bulk import
- Event streaming
- File JSON besar
Unknown Field Handling #
Secara default, field tidak dikenal akan diabaikan.
Untuk strict mode:
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
Best practice untuk public API agar client tidak kirim field aneh.
JSON Number Handling #
Default behavior:
Semua number di-decode menjadi float64 jika menggunakan interface{}.
Solusi:
decoder.UseNumber()
Agar menjadi json.Number.
Dynamic JSON (map[string]interface{}) #
Digunakan jika schema tidak tetap.
var data map[string]interface{}
json.Unmarshal(b, &data)
Masalah:
- Type assertion berantakan
- float64 untuk semua angka
- Tidak type-safe
Best practice: hindari kecuali benar-benar perlu.
Validasi JSON #
Biasanya dikombinasikan dengan validator.
Contoh:
- github.com/go-playground/validator
Flow umum:
- Decode
- Validate struct
- Return error detail
Error Handling Best Practice #
Jangan expose error mentah ke client.
Pattern:
if err := decoder.Decode(&req); err != nil {
log.Error(err)
return errorResponse("INVALID_JSON")
}
Pisahkan:
- internal error
- public error message
Performance Consideration #
Masalah utama encoding/json:
- Reflection heavy
- Allocations tinggi
Alternatif lebih cepat:
- jsoniter
- easyjson
- sonic (bytedance)
Namun tradeoff:
- Tambah dependency
- Compatibility
- Maintainability
Untuk sebagian besar API bisnis, encoding/json sudah cukup.
Common Mistakes #
- Lupa pointer di Unmarshal
- Tidak menutup Body
- Tidak validasi input
- Menggunakan map[string]interface{} berlebihan
- Tidak membedakan zero value vs missing field
- Menggunakan struct tanpa tag JSON
- Tidak menggunakan DisallowUnknownFields untuk public API
Design Pattern di Production #
1. Separate DTO dan Domain Model #
Jangan expose struct database langsung.
DTO ↔ Mapper ↔ Domain ↔ Repository
2. Gunakan Pointer untuk PATCH #
3. Gunakan Versioning #
/v1/users
/v2/users
4. Logging JSON Request (hati-hati PII) #
Penutup #
JSON handling di Golang terlihat sederhana, tetapi di production ada banyak detail penting:
- Pointer vs value
- Strict decoding
- Performance
- Validation
- Custom marshal
Menguasai JSON di Go bukan hanya soal Marshal dan Unmarshal, tetapi tentang:
- API design
- Data consistency
- Backward compatibility
- Security
- Observability
Jika digunakan dengan benar, Go menyediakan ekosistem JSON yang stabil, aman, dan cukup performant untuk sebagian besar sistem backend modern.