Go JSON
Definisi singkat
encoding/json adalah paket standar Go untuk mengubah data Go menjadi JSON dan sebaliknya. Untuk API kecil sampai menengah, paket ini biasanya jadi pilihan pertama karena tidak menambah dependency, cukup stabil, dan sudah familiar di ekosistem Go.
Paket ini cocok dipakai saat kamu perlu:
- decode request body JSON ke struct
- encode response Go ke JSON
- memetakan field dengan struct tag
json - menangani data yang sebagian strukturnya dinamis lewat
json.RawMessage - mengontrol perilaku serialisasi dengan
omitempty, custom marshaller, atau tipe khusus
Kalau kebutuhanmu hanya parsing dan serialisasi JSON standar, encoding/json biasanya sudah cukup. Kamu baru perlu library tambahan kalau ada tuntutan khusus seperti performa ekstrem, API contract yang sangat kompleks, atau fitur khusus schema/validation yang tidak enak ditangani manual.
Kapan pakai encoding/json
Pakai encoding/json kalau:
- payload JSON relatif normal dan bentuknya jelas
- kamu ingin dependency minimal
- struct Go bisa mewakili kontrak data dengan cukup baik
- kamu ingin kode yang mudah dipahami developer Go lain
- kebutuhan utama adalah maintainability, bukan trik serialisasi yang sangat khusus
Jangan terlalu cepat pindah ke library lain kalau masalahmu masih bisa diselesaikan dengan:
- struct yang lebih rapi
- tag yang benar
- validasi eksplisit setelah decode
- helper kecil untuk response dan error
- tipe custom untuk field yang sensitif seperti waktu, angka desimal, atau ID
Alur kerja yang sehat: parse → validate → marshal
Yang paling aman biasanya bukan “decode lalu langsung dipakai”, tapi decode dulu, validasi, baru marshal response.
Urutan ini penting karena:
- decode hanya memeriksa bentuk data, bukan makna bisnisnya
- validasi memastikan payload masuk akal untuk domain aplikasi
- marshal keluar harus memakai struktur yang konsisten, bukan data setengah jadi
Struktur dasar decode JSON
Contoh paling umum adalah decode request body ke struct.
package handlers
import (
"encoding/json"
"net/http"
)
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
func CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
dec := json.NewDecoder(r.Body)
if err := dec.Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
// lanjut validasi
}json.NewDecoder vs json.Unmarshal
Pilih json.NewDecoder kalau data datang dari stream seperti request body.
Pilih json.Unmarshal kalau kamu sudah punya []byte di memori dan ingin decode dari buffer yang sudah utuh.
Perbedaannya praktis:
Decodercocok untuk streaming inputUnmarshalcocok untuk data yang sudah dibaca penuhDecoderlebih natural untuk HTTP handlerUnmarshallebih sering dipakai untuk data dari file, cache, atau test fixture
Kalau kamu sedang menangani request HTTP, json.NewDecoder(r.Body) biasanya pilihan yang paling pas.
Struct tags: cara kerja dan cara pakainya
Tag json memberi tahu encoder/decoder nama field JSON yang dipakai.
type User struct {
ID int `json:"id"`
FullName string `json:"full_name"`
IsActive bool `json:"is_active"`
}Aturan yang perlu diingat
- nama field JSON diambil dari tag, bukan dari nama field Go, kalau tag ada
- kalau tag tidak ada,
encoding/jsonmemakai nama field Go dengan aturan defaultnya - field yang diawali huruf kecil di Go tidak akan diekspor, jadi tidak ikut encode/decode
- tag harus konsisten dengan kontrak API, karena typo bisa diam-diam bikin field hilang
omitempty
omitempty membuat field tidak ikut di-encode kalau nilainya zero value.
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
Email *string `json:"email,omitempty"`
}Hal yang perlu diperhatikan:
- untuk
string, zero value adalah string kosong - untuk
int, zero value adalah0 - untuk
bool, zero value adalahfalse - untuk slice, map, pointer, atau interface, zero value sering berarti
nil
Kapan omitempty membantu
- response opsional yang tidak perlu dipaksakan tampil
- payload patch/update yang membedakan field hadir vs field kosong
- output API yang ingin tetap ringkas
Kapan omitempty berbahaya
omitempty bisa menyembunyikan perbedaan antara:
- field memang tidak diisi
- field diisi dengan zero value yang valid
Contoh kasus:
0bisa berarti “tidak ada stok” atau bisa juga “belum diisi”falsebisa berarti status nonaktif, tapi bisa juga berarti default tak sengaja""bisa berarti string kosong yang valid atau field hilang
Kalau perbedaan itu penting, gunakan pointer (*int, *bool, *string) atau tipe custom agar kamu bisa membedakan antara “tidak ada” dan “ada nilainya”.
Validasi style: decode dulu, validasi eksplisit setelahnya
Di Go, validasi yang paling mudah dirawat biasanya bukan validasi yang terlalu magis. Decode payload dulu ke struct input, lalu validasi dengan aturan yang terlihat jelas.
Prinsip validasi yang sehat
- validasi bentuk data setelah decode
- validasi aturan domain secara eksplisit
- jangan campur decoding, parsing, dan business logic terlalu awal
- error validasi harus bisa menjelaskan field mana yang salah
- validasi sebaiknya dilakukan sebelum data dipakai untuk query, persist, atau side effect
Contoh style validasi manual
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Birthday string `json:"birthday"`
}
func (r CreateUserRequest) Validate() error {
if r.Name == "" {
return fmt.Errorf("name is required")
}
if r.Email == "" {
return fmt.Errorf("email is required")
}
if !strings.Contains(r.Email, "@") {
return fmt.Errorf("email is invalid")
}
return nil
}Pola seperti ini enak dibaca karena:
- input struct hanya memuat data
- method
Validatememuat aturan - handler bisa tetap tipis
Kapan pakai package validation
Package tambahan boleh dipakai kalau:
- form/payload punya banyak field
- ada banyak rule yang berulang
- kamu butuh deklarasi rule yang konsisten
- tim memang sudah menyepakati library itu
Tapi tetap jaga prinsipnya: validasi harus mudah dipahami dari kode, bukan tersembunyi di magic yang susah dilacak saat debug.
Jangan validasi di tag saja
Tag validasi bisa nyaman, tapi jangan sampai semua logika bisnis dipindah ke anotasi. Saat ada aturan yang tidak trivial, kode eksplisit tetap lebih aman untuk maintenance.
json.RawMessage: untuk payload yang belum ingin diurai penuh
json.RawMessage berguna kalau sebagian payload bersifat dinamis atau baru mau diparse setelah kita tahu tipe/branch-nya.
type Envelope struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}Kapan RawMessage dipakai
- event envelope dengan payload berbeda per
type - audit log atau webhook yang isi detailnya bervariasi
- data nested yang ingin ditunda parsing-nya
- pipeline yang perlu menyimpan JSON asli sebelum diproses lanjut
Contoh pola decode bertahap
var env Envelope
if err := json.Unmarshal(data, &env); err != nil {
return err
}
switch env.Type {
case "user_created":
var payload UserCreatedPayload
if err := json.Unmarshal(env.Payload, &payload); err != nil {
return err
}
case "user_deleted":
var payload UserDeletedPayload
if err := json.Unmarshal(env.Payload, &payload); err != nil {
return err
}
}RawMessage sangat berguna untuk contract yang heterogen, tapi jangan dipakai kalau struktur data sebenarnya sudah jelas dari awal. Kalau bisa langsung decode ke struct yang tegas, itu biasanya lebih mudah dirawat.
Marshal response: output harus konsisten
Saat mengubah data Go ke JSON, pastikan bentuk output stabil.
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email *string `json:"email,omitempty"`
}
b, err := json.Marshal(UserResponse{
ID: 1,
Name: "Ayu",
})
if err != nil {
return err
}Hal yang sering dilupakan saat marshal
- field private tidak ikut keluar
omitemptybisa menghilangkan field yang sebenarnya diharapkan client- pointer
nilakan memengaruhi bentuk output - custom type bisa punya perilaku marshal sendiri
- urutan field di JSON object tidak dijamin untuk logika bisnis; jangan andalkan ordering untuk validasi atau compare string mentah
Jebakan umum saat parse/serialize data
1. Menganggap decode JSON otomatis memvalidasi bisnis
Decode hanya memastikan JSON bisa dibaca dan dicocokkan ke struct. Payload yang secara bentuk valid belum tentu valid secara domain.
2. Lupa bahwa field non-exported tidak ikut terisi
type User struct {
id int `json:"id"`
Name string `json:"name"`
}Field id tidak akan diisi karena huruf awalnya kecil. Ini sering jadi sumber bug yang terlihat seperti “JSON-nya salah”, padahal masalahnya ada di visibility field Go.
3. Salah memaknai zero value
Saat decode, field yang tidak hadir akan jatuh ke zero value.
- string jadi
"" - int jadi
0 - bool jadi
false - slice/map jadi
nil
Kalau ketiganya punya makna berbeda dalam domainmu, jangan pakai tipe value biasa secara sembrono.
4. Mengabaikan error dari decoder/encoder
Kalau Decode gagal tapi kode tetap lanjut, struct bisa terisi sebagian. Ini berbahaya karena bug-nya terlihat seperti data valid padahal hanya setengah terisi.
5. Tidak membatasi input
Untuk HTTP request, body besar perlu dibatasi lebih awal supaya server tidak dipaksa membaca payload yang tidak semestinya.
6. Mengandalkan JSON string compare
Untuk pengecekan data, jangan terlalu sering compare string JSON mentah. Lebih aman compare struct hasil decode atau normalize object terlebih dahulu.
7. Terlalu percaya pada interface{}
map[string]any memang fleksibel, tapi cepat menjadi area bug kalau bentuk data sebenarnya sudah jelas. Pakai struct selama memungkinkan.
Time handling: format, parsing, dan timezone
Time adalah area yang sering bikin bug diam-diam. Yang terlihat sederhana di JSON bisa berubah jadi masalah saat timezone, format, atau storage masuk ke dalam cerita.
Prinsip umum
- simpan waktu dalam format yang jelas dan konsisten
- pilih satu timezone referensi untuk penyimpanan, biasanya UTC
- gunakan format yang disepakati untuk input/output
- jangan parse waktu dengan asumsi locale yang samar
- bedakan waktu untuk penyimpanan dan waktu untuk tampilan
Struct time di Go
Go sudah punya time.Time di standard library.
type Event struct {
CreatedAt time.Time `json:"created_at"`
}Kalau field ini di-marshal ke JSON, output default mengikuti format waktu RFC 3339/ISO-ish dari encoding/json untuk time.Time.
Kapan pakai format string sendiri
Pakai format string sendiri kalau:
- kontrak API sudah ditentukan format khusus
- input dari sistem eksternal tidak mengikuti RFC 3339
- kamu perlu tanggal saja, bukan timestamp lengkap
- kamu ingin output yang ringkas untuk UI tertentu
Contoh parsing tanggal custom:
const layout = "2006-01-02"
t, err := time.Parse(layout, "2026-08-27")
if err != nil {
return err
}Ingat: layout di Go bukan contoh tanggal biasa. 2006-01-02 adalah pola referensi yang wajib dihafal.
Timezone
Kalau kamu parse waktu tanpa timezone, hasilnya bisa bergantung pada konteks parsing.
Yang aman biasanya:
- pakai UTC untuk storage dan logic internal
- konversi ke timezone lokal hanya saat presentasi
- gunakan
time.LoadLocationsaat memang perlu zona tertentu - pastikan input timestamp sudah jelas membawa offset atau timezone
Contoh konversi:
loc, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return err
}
local := t.In(loc)Hati-hati dengan input tanpa offset
Timestamp seperti 2026-08-27T10:00:00 tanpa offset bisa ambigu. Akan lebih aman kalau API mengirim:
2026-08-27T10:00:00Z2026-08-27T10:00:00+07:00
Kalau kamu mendesain API, biasakan minta offset eksplisit.
Custom time type kalau format API tidak cocok
Kalau format JSON yang dibutuhkan berbeda dari default time.Time, buat tipe custom.
type DateOnly struct {
time.Time
}
func (d DateOnly) MarshalJSON() ([]byte, error) {
return []byte("\"" + d.Time.Format("2006-01-02") + "\""), nil
}
func (d *DateOnly) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), "\"")
t, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
d.Time = t
return nil
}Pola ini berguna kalau API memang harus stabil pada format tertentu dan kamu tidak mau menyebarkan parsing layout ke seluruh codebase.
Contoh alur handler yang lebih aman
type CreateEventRequest struct {
Title string `json:"title"`
StartsAt time.Time `json:"starts_at"`
EndsAt time.Time `json:"ends_at"`
}
func (r CreateEventRequest) Validate() error {
if r.Title == "" {
return fmt.Errorf("title is required")
}
if r.StartsAt.IsZero() {
return fmt.Errorf("starts_at is required")
}
if r.EndsAt.IsZero() {
return fmt.Errorf("ends_at is required")
}
if r.EndsAt.Before(r.StartsAt) {
return fmt.Errorf("ends_at must be after starts_at")
}
return nil
}Di sini kita memisahkan:
- decode JSON
- validasi required field
- validasi relasi antar waktu
- logic bisnis berikutnya
Itu jauh lebih mudah debug daripada handler yang langsung memproses semuanya sekaligus.
Jebakan umum pada time handling
1. Menganggap semua waktu lokal itu sama
Server, client, dan database bisa berada di timezone yang berbeda. Kalau timezone tidak eksplisit, bug muncul di laporan, scheduler, dan perhitungan deadline.
2. Menyimpan waktu tampilan sebagai waktu utama
Jangan simpan “waktu yang dilihat user” sebagai sumber kebenaran kalau seharusnya kamu menyimpan instant yang absolut.
3. Parse tanpa layout yang benar
time.Parse sangat ketat. Salah layout sedikit saja bisa bikin parsing gagal atau hasil yang tidak diharapkan.
4. Lupa normalisasi ke UTC
Kalau internal logic campur-campur zona waktu, perbandingan dan sorting bisa membingungkan.
5. Menggunakan string waktu untuk perhitungan
Waktu untuk komparasi dan arithmetic sebaiknya tetap di time.Time, bukan string.
Ringkasan keputusan praktis
Kalau kamu sedang membuat JSON handler Go, pakai urutan pikir ini:
- apakah payload cocok langsung ke struct?
- field mana yang wajib dan mana yang opsional?
- perlu
omitemptyatau pointer untuk membedakan kosong vs tidak ada? - ada bagian payload yang dinamis dan butuh
RawMessage? - apakah waktu perlu format custom atau cukup
time.Time? - timezone mana yang dipakai untuk storage dan mana untuk display?
- apakah validasi sudah dipisahkan dari decode?
Kesimpulan
encoding/jsonsudah cukup untuk kebanyakan API Go yang sehat.- Struct tag harus konsisten, dan
omitemptydipakai dengan sadar karena bisa menyembunyikan zero value. json.RawMessagecocok untuk payload dinamis atau parsing bertahap.- Validasi yang rapi biasanya dilakukan setelah decode, lewat style yang eksplisit dan mudah dibaca.
- Time handling paling aman kalau formatnya jelas, timezone-nya eksplisit, dan storage internal memakai referensi yang konsisten seperti UTC.
- Banyak bug JSON di Go bukan berasal dari encoder/decoder-nya, tapi dari asumsi yang terlalu optimistis saat data masuk dan keluar.