Analisis perubahan perilaku encoding/json/v2 pada Go 1.25 dan lapisan kompatibilitas v1

Paket encoding/json v2 di Go merupakan implementasi baru yang dirancang untuk memperbaiki sejumlah kelemahan v1, yaitu kurangnya konsistensi, perilaku yang mengejutkan, dan masalah performa. Fitur ini bersifat eksperimental dan diaktifkan melalui build tag goexperiment.jsonv2.
Hal yang paling penting adalah ketika v2 diaktifkan, v1 beroperasi sebagai lapisan kompatibilitas yang meniru perilaku v1 di atas implementasi v2. Hal ini dilakukan melalui fungsi DefaultOptionsV1() dalam file v2_v1.go. Dengan demikian, v2 menyediakan opsi yang memungkinkan perilaku v1 direproduksi secara lengkap, sekaligus menawarkan perilaku default baru yang lebih ketat dan dapat diprediksi.
Tujuan utama v2 adalah sebagai berikut.
- Peningkatan ketepatan dan prediktabilitas: Dengan menerapkan aturan default yang lebih ketat (misalnya, sensitivitas huruf besar/kecil dan larangan kunci duplikat), v2 mengurangi perilaku yang tidak diharapkan.
- Peningkatan performa: Mesin parsing dan encoding dirancang ulang untuk meningkatkan efisiensi.
- Perluasan fleksibilitas dan kendali: Sistem opsi yang terperinci diperkenalkan agar pengembang dapat mengontrol cara pemrosesan JSON secara rinci.
Perbedaan Perilaku dan Makna Utama
Perbedaan perilaku antara v1 dan v2 disusun per item dengan berfokus pada file v2_test.go.
1. Pencocokan Nama Field (Sensitivitas Huruf Besar/Kecil)
- Perilaku v1: Saat meng-unmarshal anggota objek JSON ke field struct Go, pencocokan dilakukan tanpa memperhatikan huruf besar/kecil (case-insensitive). Baik
"FirstName"maupun"firstname"akan dipetakan ke fieldFirstName. - Perilaku v2: Secara default, pencocokan peka terhadap huruf besar/kecil (case-sensitive), sehingga hanya field yang namanya cocok persis yang dipetakan.
- Alasan perubahan: Pencocokan yang mengabaikan huruf besar/kecil dapat menjadi penyebab perilaku yang tidak diharapkan, serta menimbulkan penurunan performa saat memproses field yang tidak cocok. v2 mengadopsi perilaku yang lebih jelas dan dapat diprediksi sebagai default.
- Opsi terkait: Di v2, pengabaian huruf besar/kecil dapat diaktifkan secara eksplisit per field menggunakan opsi tag
json:"...,case:ignore", atau diterapkan secara global denganjson.MatchCaseInsensitiveNames(true).
2. Perubahan Makna Opsi Tag omitempty
- Perilaku v1: Field dihilangkan berdasarkan "keadaan kosong (empty)" dari nilai Go. "Keadaan kosong" di sini berarti
false,0, pointer/interfacenil, serta array/slice/map/string dengan panjang nol. - Perilaku v2: Field dihilangkan berdasarkan "keadaan kosong" dari nilai JSON yang dihasilkan encoding. Artinya, field akan dihilangkan jika di-encode menjadi
null,"",{}, atau[]. - Alasan perubahan: Definisi v1 bergantung pada sistem tipe Go. v2 menggunakan sistem tipe JSON sebagai acuan sehingga memberikan perilaku yang lebih konsisten. Sebagai contoh, pada v1 nilai
falsepada tipebooldihilangkan, sedangkan pada v2falsemerupakan nilai JSON yang tidak kosong sehingga tidak dihilangkan. Di v2, opsiomitzeroditambahkan untuk menggantikan perilakuomitemptyv1 yang diterapkan pada0ataufalse. - Opsi terkait: Untuk memperoleh perilaku yang sama dengan v1 di v2, gunakan opsi
json.OmitEmptyWithLegacyDefinition(true).
3. Perubahan Perilaku Opsi Tag string
- Perilaku v1: Diterapkan pada field bertipe angka, boolean, dan string. Nilai tersebut di-encode ulang di dalam string JSON (contoh:
int(42)menjadi"42"). Penerapan tidak bersifat rekursif pada nilai di dalam tipe komposit (slice, map, dan sebagainya). - Perilaku v2: Hanya diterapkan pada tipe angka, dan bersifat rekursif. Artinya, angka di dalam slice seperti
[]intjuga akan di-encode sebagai string JSON. - Alasan perubahan: Kegunaan utama opsi
stringadalah merepresentasikan angka sebagai string untuk mencegah hilangnya presisi pada integer 64-bit. Perilaku v1 terbatas dan kurang konsisten. v2 berfokus pada kegunaan inti ini dan memperluas perilakunya secara rekursif agar lebih bermanfaat. - Opsi terkait: Perilaku v1 dapat ditiru dengan opsi
json.StringifyWithLegacySemantics(true).
4. Marshal Slice dan Map nil
- Perilaku v1: Slice
nildan mapnildi-marshal menjadinull. - Perilaku v2: Secara default, slice
nildi-marshal menjadi[](array kosong), dan mapnilmenjadi{}(objek kosong). - Alasan perubahan:
niladalah detail implementasi bahasa Go, dan memperlihatkannya dalam format JSON yang tidak bergantung pada bahasa tidak diinginkan.[]dan{}merupakan representasi koleksi kosong yang lebih umum. - Opsi terkait: Di v2, marshal menjadi
nullseperti v1 dapat dilakukan dengan opsijson.FormatNilSliceAsNull(true)ataujson.FormatNilMapAsNull(true).
5. Unmarshal Array
- Perilaku v1: Saat melakukan unmarshal ke array Go (
[N]T), tidak terjadi error meskipun panjang array JSON berbeda dengan panjang array Go. Jika array JSON lebih pendek, sisa ruang diisi dengan nilai nol, dan jika lebih panjang, kelebihannya dibuang. - Perilaku v2: Panjang array JSON harus tepat sama dengan panjang array Go. Jika tidak, akan terjadi error.
- Alasan perubahan: Di Go, panjang array berukuran tetap sering kali memiliki makna yang penting. Perilaku v1 dapat menyebabkan kehilangan data secara diam-diam. v2 menerapkan aturan yang lebih ketat untuk meningkatkan ketepatan.
- Opsi terkait: Perilaku v1 dapat ditiru dengan opsi
json.UnmarshalArrayFromAnyLength(true).
6. Penanganan time.Duration
- Perilaku v1:
time.Durationsecara internal diperlakukan sebagaiint64dan di-encode sebagai angka JSON dalam satuan nanodetik. - Perilaku v2: Di-encode sebagai string JSON dalam format seperti
"1h2m3s"dengan menggunakan metodetime.Duration.String(). - Alasan perubahan: Angka nanodetik kurang mudah dibaca, sedangkan representasi string standar dari
time.Durationlebih berguna. - Opsi terkait: Perilaku v1 dapat digunakan melalui opsi tag
json:",format:nano"atau opsijson.FormatTimeWithLegacySemantics(true).
7. Penanganan UTF-8 Tidak Valid
- Perilaku v1: Saat marshal/unmarshal, byte UTF-8 yang tidak valid di dalam string diganti secara diam-diam dengan karakter pengganti Unicode (
\uFFFD). - Perilaku v2: Secara default, ketika menemukan UTF-8 yang tidak valid, akan dikembalikan error.
- Alasan perubahan: Untuk mencegah kerusakan data yang tidak disadari serta mengikuti standar JSON yang lebih ketat (RFC 7493).
- Opsi terkait: Perilaku v1 dapat ditiru dengan opsi
jsontext.AllowInvalidUTF8(true).
8. Penanganan Nama Anggota Objek yang Duplikat
- Perilaku v1: Membolehkan anggota dengan nama yang sama muncul lebih dari sekali dalam sebuah objek JSON. Nilai yang muncul terakhir akan menimpa nilai sebelumnya.
- Perilaku v2: Secara default, jika terdapat nama anggota yang duplikat, akan dikembalikan error.
- Alasan perubahan: Standar RFC 8259 tidak mendefinisikan perilaku untuk nama duplikat sehingga implementasi yang berbeda dapat berperilaku berbeda. Hal ini dapat menjadi penyebab kerentanan keamanan. v2 secara eksplisit menolaknya untuk meningkatkan ketepatan dan keamanan.
- Opsi terkait: Perilaku v1 dapat ditiru dengan opsi
jsontext.AllowDuplicateNames(true).
Perbedaan Implementasi dan Arsitektur
- v1: Sangat bergantung pada
decodeStatedidecode.godan mesin state (state machine) yang ditulis secara manual discanner.go. Ini adalah struktur monolitik dengan logika parsing dan analisis semantik yang saling terikat erat. - v2: Arsitekturnya lebih modular.
encoding/json/jsontext: Menyediakan tokenizer JSON tingkat rendah dan berperforma tinggi, yaituDecoder, serta encoderEncoder. Paket ini hanya berfokus pada aspek sintaksis JSON.encoding/json/v2: Berdasarkanjsontext, paket ini menangani konversi semantik antara tipe Go dan nilai JSON.- Melalui pemisahan ini, analisis sintaksis dan semantik dipisahkan sehingga kejelasan kode dan performa meningkat.
API dan Fitur Baru v2
v2 menyediakan kendali yang sangat fleksibel melalui sistem json.Options.
json.Options: Kumpulan opsi yang mengubah perilaku marshal/unmarshal.json.JoinOptions(...): Menggabungkan beberapa opsi menjadi satu.WithMarshalers/WithUnmarshalers: Fitur yang kuat untuk menyisipkan logika serialisasi/deserialisasi pada tipe tertentu tanpa perlu mengimplementasikan interfaceMarshaler/Unmarshaler. Fitur ini sangat berguna saat menangani tipe dari paket eksternal.- Opsi baru:
RejectUnknownMembers,Deterministic(false),FormatNilSliceAsNull, dan lainnya memungkinkan kendali perilaku yang tidak mungkin dilakukan di v1.
Kesimpulan
encoding/json v2 merupakan implementasi modern yang secara signifikan meningkatkan ketepatan, performa, dan fleksibilitas berdasarkan pengalaman dari v1. Meskipun perilaku default menjadi lebih ketat, sistem Options yang canggih mendukung seluruh perilaku v1 secara lengkap sehingga kompatibilitas dengan kode yang ada tetap terjaga dan keunggulan v2 dapat diadopsi secara bertahap.
- Untuk proyek baru, disarankan menggunakan v2 sebagai default.
- Untuk proyek yang sudah ada, dapat digunakan
jsonv1apa adanya, atau bermigrasi kejsonv2sambil secara bertahap memperkenalkan perilaku v2 yang lebih ketat melaluiDefaultOptionsV1().