Cara Migrasi ke encoding/json/v2 di Go 1.27: Panduan Praktis dengan Contoh Kode
Oleh Tim APIPedia, Tim Engineering APIPedia · Terbit · 9 menit baca
Go Blog resmi merilis panduan JSON API baru. Pelajari beda v1 dan v2, buat klien API dan webhook ketat, lalu migrasi aman di Go 1.27.

encoding/json/v2 adalah paket JSON baru di pustaka standar Go yang stabil sejak Go 1.27, lengkap dengan paket pendamping encoding/json/jsontext. Pada 4 November 2026, tim Go menerbitkan artikel "A new Go API for JSON" beserta panduan migrasinya. Ini penting karena v2 lebih ketat secara default dan proses unmarshal-nya jauh lebih cepat.
Panduan ini praktis: kita pasang Go 1.27, melihat perbedaan perilaku v1 dan v2 dengan kode yang bisa langsung dijalankan, membuat klien API dan handler webhook yang ketat, lalu menyusun langkah migrasi yang aman untuk proyek lama. Semua contoh sudah kami uji dengan go1.27.0 linux/amd64.
Apa yang sebenarnya berubah di Go 1.27?
Go 1.27 dirilis 19 Agustus 2026. Di versi ini encoding/json/v2 dan encoding/json/jsontext tidak lagi berstatus eksperimen. Paket lama encoding/json tetap ada dan tetap didukung tanpa batas waktu, tetapi kini berjalan di atas mesin v2 dengan seluruh opsi kompatibilitas v1 dinyalakan.
Jadi ada dua kabar sekaligus:
- Kode lama tetap jalan. Anda tidak wajib mengganti import.
- Kode baru sebaiknya memakai v2. Default-nya lebih aman: UTF-8 tidak valid dan nama key ganda langsung ditolak.

API inti v2 terlihat seperti ini (dikutip dari Go Blog):
func Marshal(in any, opts ...Options) (out []byte, err error)
func MarshalWrite(out io.Writer, in any, opts ...Options) error
func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error
func Unmarshal(in []byte, out any, opts ...Options) error
func UnmarshalRead(in io.Reader, out any, opts ...Options) error
func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error
Perhatikan parameter opts ...Options. Di v1, mengatur perilaku decoder berarti membuat json.Decoder lalu memanggil method satu per satu. Di v2, opsi cukup ditempel di argumen terakhir.
Langkah 1: Bagaimana menyiapkan Go 1.27?
Cek versi Go Anda, lalu naikkan versi di go.mod proyek:
go version
# go version go1.27.0 linux/amd64
# naikkan versi bahasa di go.mod
go mod edit -go=1.27
go mod tidy
Kalau toolchain lokal masih lama, unduh dari go.dev/dl. Tidak perlu GOEXPERIMENT apa pun lagi: paket v2 langsung tersedia.
Langkah 2: Apa beda perilaku v1 dan v2 dalam praktik?
Cara tercepat memahami v2 adalah membandingkannya langsung. Simpan kode berikut sebagai main.go:
package main
import (
jsonv1 "encoding/json"
"encoding/json/v2"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Tags []string `json:"tags"`
}
func main() {
// 1. Key duplikat
dup := []byte(`{"name":"Budi","name":"Admin","age":30}`)
var a, b User
e1 := jsonv1.Unmarshal(dup, &a)
e2 := json.Unmarshal(dup, &b)
fmt.Printf("v1: %+v err=%v\nv2: %+v err=%v\n", a, e1, b, e2)
// 2. Nama key beda huruf besar/kecil
up := []byte(`{"NAME":"Sari","AGE":25}`)
var c, d User
jsonv1.Unmarshal(up, &c)
json.Unmarshal(up, &d)
fmt.Printf("v1: %+v\nv2: %+v\n", c, d)
// 3. Slice nil
o1, _ := jsonv1.Marshal(User{Name: "Rina"})
o2, _ := json.Marshal(User{Name: "Rina"})
fmt.Println(string(o1))
fmt.Println(string(o2))
}
Jalankan dengan go run .. Ini output asli dari uji kami:
v1: {Name:Admin Age:30 Tags:[]} err=<nil>
v2: {Name:Budi Age:0 Tags:[]} err=jsontext: duplicate object member name "name"
v1: {Name:Sari Age:25 Tags:[]}
v2: {Name: Age:0 Tags:[]}
{"name":"Rina","age":0,"tags":null}
{"name":"Rina","age":0,"tags":[]}
Baris pertama yang paling penting. Di v1, payload dengan "name" ganda diterima dan nilai terakhir yang menang. Kalau satu komponen sistem membaca key pertama dan komponen lain membaca key terakhir, penyerang bisa menyelundupkan nilai. Celah semacam ini pernah terjadi di CouchDB (CVE-2017-12635). v2 menolaknya sejak awal.
![Tabel perbandingan perilaku encoding/json v1 dan v2: key duplikat diterima di v1 tetapi error di v2, UTF-8 tidak valid diganti karakter pengganti di v1 tetapi error di v2, pencocokan nama key case-insensitive di v1 dan case-sensitive di v2, slice dan map nil menjadi null di v1 dan [] atau {} di v2, makna omitempty berubah, time.Duration error di v2, beserta opsi kompatibel untuk tiap kasus](https://supabase.apipedia.id/storage/v1/object/public/blog-media/posts/2026/11/cara-migrasi-go-json-v2-diagram-1-796bbc14.webp)
Ringkasan perbedaan dan opsi pengembaliannya
| Kasus | v1 | v2 (default) | Opsi untuk perilaku lama |
|---|---|---|---|
| Key duplikat | Diterima, nilai terakhir menang | Error | jsontext.AllowDuplicateNames(true) |
| UTF-8 tidak valid | Diganti karakter pengganti | Error | jsontext.AllowInvalidUTF8(true) |
| Nama key beda kapital | Tetap cocok | Tidak cocok | json.MatchCaseInsensitiveNames(true) |
| Slice/map nil | null | [] / {} | json.FormatNilSliceAsNull(true), json.FormatNilMapAsNull(true) |
omitempty | Buang nilai nol Go | Buang nilai JSON kosong (null, "", [], {}) | Pakai tag omitzero |
time.Duration | Angka nanodetik | Error, tidak ada format default | Lihat panduan migrasi resmi |
Satu jebakan kecil: di v2, omitempty untuk int bernilai 0 tidak lagi membuang field, karena angka 0 bukan "nilai JSON kosong". Kalau maksud Anda membuang nilai nol Go, ganti dengan omitzero.
Langkah 3: Bagaimana memakai v2 untuk memanggil API?
Kasus paling umum di backend adalah memanggil API lain lalu men-decode respons. Contoh ini memanggil endpoint hari libur nasional APIPedia, yang memakai amplop standar { is_success, message, data }. Ada dua fitur v2 yang terpakai:
json.UnmarshalReadmembaca langsung dariresp.Bodytanpaio.ReadAll.- Tag
json:",unknown"pada field bertipejsontext.Valuemenampung field baru dari server, jadi tidak ada data yang hilang diam-diam.
package main
import (
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"log"
"net/http"
"os"
"time"
)
// Amplop standar APIPedia: { is_success, message, data }
type Envelope[T any] struct {
IsSuccess bool `json:"is_success"`
Message string `json:"message"`
Data T `json:"data"`
}
type Libur struct {
Tanggal string `json:"tanggal"`
Keterangan string `json:"keterangan"`
Jenis string `json:"jenis"`
Hari string `json:"hari"`
SisaHari int `json:"sisa_hari"`
// Field baru dari server disimpan di sini, tidak dibuang
Lainnya jsontext.Value `json:",unknown"`
}
func main() {
req, _ := http.NewRequest("GET", "https://api.apipedia.id/v1/libur/berikutnya", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("APIPEDIA_KEY"))
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
var env Envelope[*Libur]
if err := json.UnmarshalRead(resp.Body, &env); err != nil {
log.Fatalf("decode gagal: %v", err)
}
if !env.IsSuccess || env.Data == nil {
log.Fatalf("API error (%d): %s", resp.StatusCode, env.Message)
}
l := env.Data
fmt.Printf("Libur berikutnya: %s, %s (%s), %d hari lagi\n",
l.Keterangan, l.Hari, l.Tanggal, l.SisaHari)
if len(l.Lainnya) > 0 {
fmt.Println("Field tambahan dari server:", string(l.Lainnya))
}
}
Jalankan dengan API key dari console.apipedia.id:
APIPEDIA_KEY=apk_xxx go run .
# Libur berikutnya: Kelahiran Yesus Kristus (Natal), Jumat (2026-12-25), 49 hari lagi
Pola yang sama berlaku untuk API apa pun. Generic Envelope[T] membuat satu tipe amplop bisa dipakai ulang untuk semua endpoint.
Langkah 4: Bagaimana membuat handler webhook yang ketat?
Di sisi server, v2 paling terasa manfaatnya untuk endpoint yang menerima data dari luar, misalnya webhook pembayaran. Kita ingin menolak payload yang aneh, bukan menerimanya diam-diam.

package main
import (
"encoding/json/v2"
"log"
"net/http"
)
type Order struct {
ID string `json:"id"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
Note string `json:"note,omitzero"`
Items []string `json:"items"`
}
type Reply struct {
OK bool `json:"ok"`
Error string `json:"error,omitzero"`
Status string `json:"status,omitzero"`
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.MarshalWrite(w, v) // tulis langsung ke ResponseWriter
}
func handleOrder(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // batasi 1 MB
var o Order
err := json.UnmarshalRead(r.Body, &o,
json.RejectUnknownMembers(true), // tolak field asing
)
if err != nil {
writeJSON(w, http.StatusBadRequest, Reply{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, Reply{OK: true, Status: "diterima " + o.ID})
}
func main() {
http.HandleFunc("POST /orders", handleOrder)
log.Println("listen :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Uji dengan empat payload berbeda:
curl -s -X POST localhost:8080/orders \
-d '{"id":"INV-1","amount":150000,"currency":"IDR","items":["kopi"]}'
# {"ok":true,"status":"diterima INV-1"}
curl -s -X POST localhost:8080/orders \
-d '{"id":"INV-1","amount":150000,"amount":1,"currency":"IDR"}'
# {"ok":false,"error":"jsontext: duplicate object member name \"amount\""}
curl -s -X POST localhost:8080/orders \
-d '{"id":"INV-2","amount":5000,"currency":"IDR","is_admin":true}'
# {"ok":false,"error":"json: cannot unmarshal JSON string into Go main.Order: unknown object member name \"is_admin\""}
curl -s -X POST localhost:8080/orders -d '{"ID":"INV-3","Amount":5000}'
# {"ok":false,"error":"json: cannot unmarshal JSON string into Go main.Order: unknown object member name \"ID\""}
Tanpa satu baris validasi manual, payload dengan amount ganda, field is_admin selundupan, dan key berhuruf besar semuanya ditolak. Untuk produksi, jangan kirim err.Error() mentah ke klien; cukup catat di log dan balas pesan umum.
Langkah 5: Bagaimana migrasi proyek lama dengan aman?
Tim Go menegaskan aplikasi v1 tidak wajib pindah. Kalau ingin pindah, lakukan bertahap:
- Naikkan ke Go 1.27 dulu tanpa mengubah import. Jalankan seluruh test. Paket
encoding/jsonsudah berjalan di mesin v2, jadi ini sendiri sudah menguji kompatibilitas. - Periksa test yang membandingkan output byte demi byte (golden file, checksum, tanda tangan). Ada laporan bahwa byte keluaran untuk UTF-8 tidak valid berubah antara Go 1.26 dan 1.27.
- Benchmark jalur panas. Hasil performa tidak seragam (lihat bagian berikut).
- Pindahkan paket satu per satu ke v2, mulai dari endpoint yang menerima input dari luar.
- Kalau butuh perilaku lama, pilih opsinya satu per satu, jangan langsung semuanya.
Contoh memakai API v2 tetapi dengan perilaku v1, atau memilih opsi tertentu saja:
import (
jsonv1 "encoding/json"
"encoding/json/jsontext"
"encoding/json/v2"
)
// API v2, perilaku persis v1
b, err := json.Marshal(v, jsonv1.DefaultOptionsV1())
// API v2, hanya izinkan key ganda (sisanya tetap ketat)
err = json.Unmarshal(data, &v, jsontext.AllowDuplicateNames(true))
// Output deterministik (urutan key map tetap) + indentasi
out, err := json.Marshal(m, json.Deterministic(true), jsontext.WithIndent(" "))
Kalau setelah naik versi ada perilaku aneh dan Anda ingin memastikan penyebabnya mesin JSON baru, ada saklar darurat GOEXPERIMENT=nojsonv2 yang mengembalikan implementasi v1 lama:
GOEXPERIMENT=nojsonv2 go test ./...
Dua catatan dari uji kami dan diskusi di issue Go #79788: saklar ini gagal dikompilasi bila kode Anda atau dependensi meng-import encoding/json/v2 atau jsontext (muncul error build constraints exclude all Go files), dan working group menyebutnya solusi jangka pendek, bukan pengaturan permanen.
Seberapa cepat v2 sebenarnya?
Klaim resmi Go Blog: Unmarshal jauh lebih cepat, dengan benchmark menunjukkan peningkatan hingga 10x, sementara Marshal kira-kira setara v1. Pengukuran independen memberi gambaran lebih bervariasi:
- Daniel Lemire (Go 1.27.0, satu core) mencatat unmarshal
twitter.jsonnaik dari 172 MB/s ke 203 MB/s hanya dengan upgrade ke Go 1.27 tanpa ubah kode, tetapicanada.jsonturun dari 128 MB/s ke 106 MB/s. Dengan API v2 langsung, unmarshal 1,5x sampai 2,3x lebih cepat dibanding implementasi lama, sedangkan marshal struct kecil sekitar 1,5x lebih lambat. - Huizhou92 (go1.27rc2, Apple M5) mencatat waktu
UnmarshalSmallturun 43,5%, tetapi unmarshal kemap[string]anynaik sekitar 48% danMarshalSmallnaik 78,8%.
Kesimpulannya: decode ke struct bertipe hampir selalu untung, sedangkan layanan yang banyak me-marshal objek kecil atau decode ke map[string]any wajib benchmark dulu. Tim Go menyebut optimasi lanjutan direncanakan untuk Go 1.28.
FAQ
Apakah saya wajib mengganti encoding/json ke v2?
Tidak. Menurut tim Go, aplikasi yang memakai v1 tetap didukung tanpa batas waktu. v1 kini diimplementasikan di atas v2 dengan opsi kompatibilitas penuh.
Apakah v1 dan v2 bisa dipakai bersamaan dalam satu proyek?
Bisa. Keduanya saling kompatibel: v2 mendukung method MarshalJSON gaya v1, dan v1 mendukung MarshalJSONTo gaya v2. Cukup beri alias import, misalnya jsonv1 "encoding/json".
Kenapa field saya kosong setelah pindah ke v2?
Kemungkinan besar karena pencocokan nama kini case-sensitive. Payload {"NAME":"..."} tidak lagi masuk ke tag json:"name". Perbaiki sumber datanya, atau pakai opsi json.MatchCaseInsensitiveNames(true).
Apa beda omitempty dan omitzero di v2?
omitzero membuang field bila nilainya nol menurut Go (0, string kosong, nil). omitempty di v2 membuang field bila hasil JSON-nya kosong: null, "", [], atau {}. Angka 0 tetap ditulis bila memakai omitempty.
Apa fungsi paket jsontext?
jsontext menangani sintaks JSON murni tanpa reflection: Encoder, Decoder, Token, dan Value. Cocok untuk streaming data besar atau meneruskan JSON mentah. json.RawMessage di v1 kini alias dari jsontext.Value.
Sumber
- The Go Blog: A new Go API for JSON (4 November 2026)
- Linuxiac: Go 1.27 Released with Generic Methods, JSON v2, and Faster Memory Allocation
- Huizhou92: encoding/json/v2 Waited Five and a Half Years to Become Go's Default
- Daniel Lemire: The new Go JSON API, twice as fast, or 1.5x slower?
- DEV Community: 7 Go jsonv2 breaks I verified in Go 1.27
- GitHub golang/go #79788: GOEXPERIMENT=nojsonv2 dan paket v2
Topik: #golang #json #tutorial #backend #api
Tentang penulis
Tim APIPedia — Tim Engineering APIPedia
Tim engineering di balik APIPedia yang membangun dan memelihara API data Indonesia - ongkir, OCR KTP, data wilayah, BMKG, hingga server MCP - dan menulis panduan integrasinya dari pengalaman langsung.