BAB 47: JSON Encoding dan Decoding

Pelajari cara mengonversi data Go ke JSON dan sebaliknya menggunakan package encoding/json — dari struct sederhana hingga penanganan data dinamis.

Di bab sebelumnya, program Go sudah bisa mem-parsing URL yang kompleks — termasuk membaca query string yang berisi parameter konfigurasi. Tapi ketika program berkomunikasi dengan API eksternal atau menyimpan data terstruktur, format yang paling sering digunakan bukan URL, melainkan JSON.

JSON (JavaScript Object Notation) sudah menjadi lingua franca pertukaran data di dunia web modern. Hampir semua API yang akan dikonsumsi program Go mengirim dan menerima data dalam format ini. Package encoding/json dari standard library Go menyediakan semua alat yang dibutuhkan untuk bekerja dengan JSON — tanpa dependensi eksternal.

Dari Struct ke JSON

Konversi dari struct Go ke JSON disebut encoding atau marshaling. Fungsi json.Marshal() menerima nilai Go apapun dan mengembalikan []byte yang berisi representasi JSON-nya.

// artikel-json.go
package main

import (
    "encoding/json"
    "fmt"
)

type Artikel struct {
    ID       int
    Judul    string
    Penulis  string
    Diterbitkan bool
}

func main() {
    artikel := Artikel{
        ID:          1,
        Judul:       "Memahami Goroutine di Go",
        Penulis:     "Budi Santoso",
        Diterbitkan: true,
    }

    hasil, err := json.Marshal(artikel)
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Println(string(hasil))
}

Output:

{"ID":1,"Judul":"Memahami Goroutine di Go","Penulis":"Budi Santoso","Diterbitkan":true}

json.Marshal() menghasilkan []byte, bukan string. Untuk menampilkan hasilnya, perlu dikonversi ke string terlebih dahulu dengan string(hasil).

Ada satu hal yang perlu diperhatikan: nama field di output JSON persis sama dengan nama field di struct, termasuk huruf kapital. Ini mungkin tidak sesuai dengan konvensi JSON yang lazimnya menggunakan camelCase atau snake_case.

JSON Tags untuk Kontrol Nama Field

Struct tag json:"..." memungkinkan kontrol penuh atas nama field yang muncul di JSON, tanpa harus mengubah nama field di struct.

// artikel-tag.go
package main

import (
    "encoding/json"
    "fmt"
)

type Artikel struct {
    ID          int    `json:"id"`
    Judul       string `json:"judul"`
    Penulis     string `json:"penulis"`
    Diterbitkan bool   `json:"diterbitkan"`
    DraftKe     int    `json:"draft_ke,omitempty"`
}

func main() {
    // artikel yang sudah diterbitkan — DraftKe kosong (0)
    terbit := Artikel{
        ID:          1,
        Judul:       "Memahami Goroutine di Go",
        Penulis:     "Budi Santoso",
        Diterbitkan: true,
    }

    // artikel draft — DraftKe diisi
    draft := Artikel{
        ID:          2,
        Judul:       "Channel dan Sinkronisasi",
        Penulis:     "Rina Wijaya",
        Diterbitkan: false,
        DraftKe:     3,
    }

    jsonTerbit, _ := json.Marshal(terbit)
    jsonDraft, _ := json.Marshal(draft)

    fmt.Println(string(jsonTerbit))
    fmt.Println(string(jsonDraft))
}

Output:

{"id":1,"judul":"Memahami Goroutine di Go","penulis":"Budi Santoso","diterbitkan":true}
{"id":2,"judul":"Channel dan Sinkronisasi","penulis":"Rina Wijaya","diterbitkan":false,"draft_ke":3}

Opsi omitempty pada field DraftKe membuat field tersebut tidak muncul di JSON ketika nilainya zero value (0 untuk integer, "" untuk string, false untuk bool, nil untuk pointer dan slice). Karena artikel pertama sudah diterbitkan dan DraftKe-nya adalah 0, field itu tidak muncul di output.

Gunakan json:"-" jika ingin field sama sekali tidak pernah muncul di JSON, terlepas dari nilainya. Berguna untuk field yang berisi data sensitif seperti password hash atau token internal.

Dari JSON ke Struct

Kebalikan dari marshaling adalah decoding atau unmarshaling. Fungsi json.Unmarshal() mengisi nilai ke struct berdasarkan JSON yang diberikan.

// artikel-decode.go
package main

import (
    "encoding/json"
    "fmt"
)

type Artikel struct {
    ID          int    `json:"id"`
    Judul       string `json:"judul"`
    Penulis     string `json:"penulis"`
    Diterbitkan bool   `json:"diterbitkan"`
}

func main() {
    data := []byte(`{
        "id": 5,
        "judul": "Defer dan Panic Recovery",
        "penulis": "Hendra Kusuma",
        "diterbitkan": true
    }`)

    var artikel Artikel
    err := json.Unmarshal(data, &artikel)
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Printf("ID      : %d\n", artikel.ID)
    fmt.Printf("Judul   : %s\n", artikel.Judul)
    fmt.Printf("Penulis : %s\n", artikel.Penulis)
    fmt.Printf("Terbit  : %v\n", artikel.Diterbitkan)
}

Output:

ID      : 5
Judul   : Defer dan Panic Recovery
Penulis : Hendra Kusuma
Terbit  : true

json.Unmarshal() membutuhkan pointer ke variabel tujuan — perhatikan &artikel. Tanpa tanda &, fungsi menerima salinan variabel dan perubahan tidak akan tersimpan.

Go cukup toleran saat decoding: field JSON yang tidak ada padanannya di struct akan diabaikan. Sebaliknya, field struct yang tidak ada di JSON tetap menyimpan zero value-nya.

Decoding JSON Array

Ketika respons API berupa array JSON, decode ke slice of struct.

// daftar-artikel.go
package main

import (
    "encoding/json"
    "fmt"
)

type Artikel struct {
    ID     int    `json:"id"`
    Judul  string `json:"judul"`
    Topik  string `json:"topik"`
}

func main() {
    data := []byte(`[
        {"id": 1, "judul": "Hello World di Go", "topik": "dasar"},
        {"id": 2, "judul": "Goroutine Pertama", "topik": "concurrency"},
        {"id": 3, "judul": "Bekerja dengan Channel", "topik": "concurrency"}
    ]`)

    var daftarArtikel []Artikel
    err := json.Unmarshal(data, &daftarArtikel)
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Printf("Total artikel: %d\n\n", len(daftarArtikel))
    for _, a := range daftarArtikel {
        fmt.Printf("[%d] %s (topik: %s)\n", a.ID, a.Judul, a.Topik)
    }
}

Output:

Total artikel: 3

[1] Hello World di Go (topik: dasar)
[2] Goroutine Pertama (topik: concurrency)
[3] Bekerja dengan Channel (topik: concurrency)

Polanya identik dengan decode ke struct tunggal — hanya variabel tujuannya yang diubah dari Artikel menjadi []Artikel.

Decoding ke Map

Tidak selalu struktur JSON diketahui di compile time. Ketika menerima JSON yang fieldnya dinamis atau tidak tetap, decode ke map[string]interface{} adalah solusinya.

// metadata-dinamis.go
package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    data := []byte(`{
        "platform": "KontenKu",
        "versi": "2.1.0",
        "fitur_aktif": ["komentar", "rating", "bookmark"],
        "batas_upload_mb": 50
    }`)

    var metadata map[string]interface{}
    err := json.Unmarshal(data, &metadata)
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Println("Platform :", metadata["platform"])
    fmt.Println("Versi    :", metadata["versi"])
    fmt.Println("Batas MB :", metadata["batas_upload_mb"])

    // mengakses array yang tersimpan sebagai []interface{}
    if fitur, ok := metadata["fitur_aktif"].([]interface{}); ok {
        fmt.Print("Fitur    : ")
        for i, f := range fitur {
            if i > 0 {
                fmt.Print(", ")
            }
            fmt.Print(f)
        }
        fmt.Println()
    }
}

Output:

Platform : KontenKu
Versi    : 2.1.0
Batas MB : 50
Fitur    : komentar, rating, bookmark

Ketika JSON di-decode ke map[string]interface{}, semua nilai numerik otomatis menjadi float64, bukan int. Ini perilaku default Go — perlu type assertion yang tepat saat menggunakannya, atau gunakan json.Decoder dengan UseNumber() jika presisi angka penting.

JSON yang Rapi dengan MarshalIndent

Output json.Marshal() tidak mengandung spasi sama sekali — ideal untuk transfer data, tapi sulit dibaca manusia. Untuk keperluan debugging atau menyimpan konfigurasi yang perlu dibaca manusia, gunakan json.MarshalIndent().

// konfigurasi-json.go
package main

import (
    "encoding/json"
    "fmt"
)

type KonfigurasiPlatform struct {
    NamaPlatform  string   `json:"nama_platform"`
    Versi         string   `json:"versi"`
    MaksBahasaMB  int      `json:"maks_unggahan_mb"`
    FiturAktif    []string `json:"fitur_aktif"`
}

func main() {
    config := KonfigurasiPlatform{
        NamaPlatform: "KontenKu",
        Versi:        "2.1.0",
        MaksBahasaMB: 50,
        FiturAktif:   []string{"komentar", "rating", "bookmark"},
    }

    hasil, err := json.MarshalIndent(config, "", "  ")
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Println(string(hasil))
}

Output:

{
  "nama_platform": "KontenKu",
  "versi": "2.1.0",
  "maks_unggahan_mb": 50,
  "fitur_aktif": [
    "komentar",
    "rating",
    "bookmark"
  ]
}

Parameter kedua MarshalIndent() adalah prefix (biasanya string kosong), dan parameter ketiga adalah string indentasi per level.

Struct Bersarang

Data JSON di dunia nyata sering punya struktur bersarang. Struct Go bisa merepresentasikannya dengan cara yang bersih.

// artikel-lengkap.go
package main

import (
    "encoding/json"
    "fmt"
)

type ProfilPenulis struct {
    Nama  string `json:"nama"`
    Email string `json:"email"`
}

type ArtikelLengkap struct {
    ID      int           `json:"id"`
    Judul   string        `json:"judul"`
    Konten  string        `json:"konten"`
    Penulis ProfilPenulis `json:"penulis"`
    Tag     []string      `json:"tag"`
}

func main() {
    data := []byte(`{
        "id": 10,
        "judul": "Memahami Interface di Go",
        "konten": "Interface adalah kontrak perilaku...",
        "penulis": {
            "nama": "Sari Dewi",
            "email": "sari@kontenku.id"
        },
        "tag": ["golang", "oop", "interface"]
    }`)

    var artikel ArtikelLengkap
    err := json.Unmarshal(data, &artikel)
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Printf("Judul    : %s\n", artikel.Judul)
    fmt.Printf("Penulis  : %s (%s)\n", artikel.Penulis.Nama, artikel.Penulis.Email)
    fmt.Printf("Tag      : %v\n", artikel.Tag)
}

Output:

Judul    : Memahami Interface di Go
Penulis  : Sari Dewi (sari@kontenku.id)
Tag      : [golang oop interface]

Go secara otomatis menangani JSON bersarang selama struktur struct cocok dengan struktur JSON. Tidak ada konfigurasi tambahan yang diperlukan.

Latihan

Latihan 1 — Round-trip JSON: Buat struct KomentarArtikel dengan field id (int), artikel_id (int), isi (string), disetujui (bool), dan dibalas_ke (int, omitempty). Buat beberapa instance, encode ke JSON, lalu decode kembali dan verifikasi hasilnya sama dengan data asli.

Latihan 2 — Parsing respons API: Simulasikan respons API dengan struktur berikut dan decode-nya ke struct yang sesuai:

{
  "sukses": true,
  "pesan": "Data berhasil diambil",
  "data": {
    "total": 100,
    "halaman": 1,
    "artikel": [{"id": 1, "judul": "Artikel Pertama"}]
  }
}

Hint: gunakan struct bersarang untuk field data.

Latihan 3 — Konfigurasi dari file JSON: Buat struct KonfigurasiAplikasi dengan beberapa field. Encode ke JSON dengan MarshalIndent, tulis hasilnya ke string, lalu decode kembali. Pastikan nilai semua field sama setelah round-trip.


Dengan encoding/json, program Go bisa berbicara dalam format yang dipahami hampir semua layanan web modern. Decode respons API, encode data ke penyimpanan, atau membangun pipeline transformasi data — semuanya bisa dilakukan dengan dua fungsi utama yang sudah dipelajari di bab ini. Di bab selanjutnya, kemampuan ini akan langsung dipakai: program Go tidak hanya menerima request sebagai server, tapi juga mengirim request ke API eksternal sebagai klien HTTP.

Referensi

  1. 1Package encoding/json — Go Standard Library Documentation
  2. 2JSON — Go by Example
  3. 3JSON and Go — The Go Blog