Go JSON Encoding Decoding Validation Time Handling
Selesai membaca dokumentasi ini?
Kembali ke rujukan utama Go (Golang) untuk melanjutkan topik lainnya.
Kembali ke rujukan utama Go (Golang) untuk melanjutkan topik lainnya.
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:
jsonjson.RawMessageomitempty, custom marshaller, atau tipe khususKalau 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.
encoding/jsonPakai encoding/json kalau:
Jangan terlalu cepat pindah ke library lain kalau masalahmu masih bisa diselesaikan dengan:
Yang paling aman biasanya bukan “decode lalu langsung dipakai”, tapi decode dulu, validasi, baru marshal response.
Urutan ini penting karena:
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
json.NewDecoder vs json.UnmarshalPilih 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:
Decoder cocok untuk streaming inputUnmarshal cocok untuk data yang sudah dibaca penuhDecoder lebih natural untuk HTTP handlerUnmarshal lebih sering dipakai untuk data dari file, cache, atau test fixtureKalau kamu sedang menangani request HTTP, json.NewDecoder(r.Body) biasanya pilihan yang paling pas.
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"`
}encoding/json memakai nama field Go dengan aturan defaultnyaomitemptyomitempty 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:
string, zero value adalah string kosongint, zero value adalah 0bool, zero value adalah falsenilomitempty membantuomitempty berbahayaomitempty bisa menyembunyikan perbedaan antara:
Contoh kasus:
0 bisa berarti “tidak ada stok” atau bisa juga “belum diisi”false bisa berarti status nonaktif, tapi bisa juga berarti default tak sengaja"" bisa berarti string kosong yang valid atau field hilangKalau perbedaan itu penting, gunakan pointer (*int, *bool, *string) atau tipe custom agar kamu bisa membedakan antara “tidak ada” dan “ada nilainya”.
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.
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Birthday string `json:"birthday"`
}
func (r CreateUserRequest) Validate() error {
if r.Name
Pola seperti ini enak dibaca karena:
Validate memuat aturanPackage tambahan boleh dipakai kalau:
Tapi tetap jaga prinsipnya: validasi harus mudah dipahami dari kode, bukan tersembunyi di magic yang susah dilacak saat debug.
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 penuhjson.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"`
}RawMessage dipakaitypevar env Envelope
if err := json.Unmarshal(data, &env); err != nil {
return err
}
switch env.Type {
case "user_created":
var payload UserCreatedPayload
if
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.
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",
omitempty bisa menghilangkan field yang sebenarnya diharapkan clientnil akan memengaruhi bentuk outputDecode hanya memastikan JSON bisa dibaca dan dicocokkan ke struct. Payload yang secara bentuk valid belum tentu valid secara domain.
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.
Saat decode, field yang tidak hadir akan jatuh ke zero value.
""0falsenilKalau ketiganya punya makna berbeda dalam domainmu, jangan pakai tipe value biasa secara sembrono.
Kalau Decode gagal tapi kode tetap lanjut, struct bisa terisi sebagian. Ini berbahaya karena bug-nya terlihat seperti data valid padahal hanya setengah terisi.
Untuk HTTP request, body besar perlu dibatasi lebih awal supaya server tidak dipaksa membaca payload yang tidak semestinya.
Untuk pengecekan data, jangan terlalu sering compare string JSON mentah. Lebih aman compare struct hasil decode atau normalize object terlebih dahulu.
interface{}map[string]any memang fleksibel, tapi cepat menjadi area bug kalau bentuk data sebenarnya sudah jelas. Pakai struct selama memungkinkan.
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.
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.
Pakai format string sendiri kalau:
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.
Kalau kamu parse waktu tanpa timezone, hasilnya bisa bergantung pada konteks parsing.
Yang aman biasanya:
time.LoadLocation saat memang perlu zona tertentuContoh konversi:
loc, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return err
}
local := t.In(loc)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:00Kalau kamu mendesain API, biasakan minta offset eksplisit.
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.
Pola ini berguna kalau API memang harus stabil pada format tertentu dan kamu tidak mau menyebarkan parsing layout ke seluruh codebase.
type CreateEventRequest struct {
Title string `json:"title"`
StartsAt time.Time `json:"starts_at"`
EndsAt time.Time `json:"ends_at"`
}
func (r CreateEventRequest)
Di sini kita memisahkan:
Itu jauh lebih mudah debug daripada handler yang langsung memproses semuanya sekaligus.
Server, client, dan database bisa berada di timezone yang berbeda. Kalau timezone tidak eksplisit, bug muncul di laporan, scheduler, dan perhitungan deadline.
Jangan simpan “waktu yang dilihat user” sebagai sumber kebenaran kalau seharusnya kamu menyimpan instant yang absolut.
time.Parse sangat ketat. Salah layout sedikit saja bisa bikin parsing gagal atau hasil yang tidak diharapkan.
Kalau internal logic campur-campur zona waktu, perbandingan dan sorting bisa membingungkan.
Waktu untuk komparasi dan arithmetic sebaiknya tetap di time.Time, bukan string.
Kalau kamu sedang membuat JSON handler Go, pakai urutan pikir ini:
omitempty atau pointer untuk membedakan kosong vs tidak ada?RawMessage?time.Time?encoding/json sudah cukup untuk kebanyakan API Go yang sehat.omitempty dipakai dengan sadar karena bisa menyembunyikan zero value.json.RawMessage cocok untuk payload dinamis atau parsing bertahap.