encoding/json/v2, Go 1.27 ile standart kütüphaneye giren ve encoding/json'un büyük bir revizyonu olan pakettir. Basit kullanımda API neredeyse aynıdır: import "encoding/json" satırını import "encoding/json/v2" yapınca json.Marshal(v) ve json.Unmarshal(b, &v) derlenmeye devam eder. Asıl fark davranıştadır: v2 nil dilimleri null yerine [] yazar, yinelenen anahtarları ve geçersiz UTF-8'i reddeder, struct alanlarını büyük-küçük harf duyarlı eşleştirir ve time.Duration için varsayılan bir gösterim tanımlamaz.
Geçmek zorunda değilsiniz: resmî geçiş rehberinin ilk cümlesi tam olarak budur. encoding/json Go 1 uyumluluk sözü kapsamındadır ve hiçbir zaman kaldırılmayacaktır. Bu yazıda iki sürüm arasındaki davranış farklarını Go 1.27.3 ile çalıştırılmış örneklerle, v1 davranışını v2 API'siyle koruyan seçenekleri, yeni MarshalWrite/UnmarshalRead fonksiyonlarını ve güvenli bir geçiş planını görüyorsunuz. Paketin temel (v1) kullanımı için encoding/json rehberine bakabilirsiniz.
v1 ile v2 arasındaki başlıca farklar

encoding/json belgesindeki "Migrating to v2" bölümü farkların tam listesini ve her birini kontrol eden seçeneği verir. En sık karşılaşacaklarınız şunlardır:
| Konu | v1 (encoding/json) | v2 (encoding/json/v2) | v1 davranışı için seçenek |
|---|---|---|---|
| nil dilim / map | null | [] / {} | FormatNilSliceAsNull, FormatNilMapAsNull |
| Alan adı eşleştirme | büyük-küçük harf duyarsız | tam ve duyarlı | MatchCaseInsensitiveNames |
| Yinelenen anahtar | kabul eder, sonuncusu kazanır | hata | jsontext.AllowDuplicateNames |
| Geçersiz UTF-8 | yer değiştirme karakteriyle sessizce değiştirir | hata | jsontext.AllowInvalidUTF8 |
| omitempty | Go sıfır değerine göre (false, 0, nil, boş) | JSON boşluğuna göre (null, "", {}, []) | OmitEmptyWithLegacySemantics |
| time.Duration | nanosaniye sayısı | varsayılan gösterim yok, hata | FormatDurationAsNano |
| Map sırası | deterministik | deterministik değil | json.Deterministic |
| HTML karakterleri | <, > ve & kaçışlanır | yalnızca gerekenler kaçışlanır | jsontext.EscapeForHTML |
| Bayt dizisi [N]byte | sayı dizisi | Base64 string | FormatByteArrayAsArray |
| Hatalı struct etiketi | sessizce yok sayılır | çalışma zamanı hatası | ReportErrorsWithLegacySemantics |
Bu listedeki her fark bir Options değeriyle kontrol edilir. Seçenekler soldan sağa uygulanır ve sonra gelen önce geleni ezer. encoding/json paketindeki DefaultOptionsV1() tüm v1 davranışlarını bir arada verir; belgeye göre v1'in Marshal ve Unmarshal fonksiyonları zaten v2 fonksiyonlarını bu seçeneklerle çağırarak çalışır. Yani v1 paketi artık v2'nin üzerinde bir uyumluluk katmanıdır.
nil dilimler ve DefaultOptionsV1
Geçiş rehberindeki ilk örnek en sık karşılaşılan farkı gösterir: takma adı olmayan bir evcil hayvanın Lakaplar alanı v1'de null, v2'de [] olarak yazılır. Aşağıda aynı değeri dört farklı yoldan serileştiriyoruz. İki paketi aynı dosyada kullanabilmek için v1'i jsonv1 adıyla içe aktardık.
package main
import (
jsonv1 "encoding/json"
"encoding/json/v2"
"fmt"
)
type Evcil struct {
Ad string
Lakaplar []string
Ozellik map[string]string
}
func main() {
e := []Evcil{{Ad: "Pamuk", Lakaplar: []string{"Pamik"}}, {Ad: "Duman"}}
b1, _ := jsonv1.Marshal(e)
fmt.Println(string(b1))
b2, _ := json.Marshal(e)
fmt.Println(string(b2))
b3, _ := json.Marshal(e, jsonv1.DefaultOptionsV1())
fmt.Println(string(b3))
b4, _ := json.Marshal(e, json.FormatNilSliceAsNull(true))
fmt.Println(string(b4))
}
// Çıktı:
// [{"Ad":"Pamuk","Lakaplar":["Pamik"],"Ozellik":null},{"Ad":"Duman","Lakaplar":null,"Ozellik":null}]
// [{"Ad":"Pamuk","Lakaplar":["Pamik"],"Ozellik":{}},{"Ad":"Duman","Lakaplar":[],"Ozellik":{}}]
// [{"Ad":"Pamuk","Lakaplar":["Pamik"],"Ozellik":null},{"Ad":"Duman","Lakaplar":null,"Ozellik":null}]
// [{"Ad":"Pamuk","Lakaplar":["Pamik"],"Ozellik":{}},{"Ad":"Duman","Lakaplar":null,"Ozellik":{}}]Üçüncü satır, v2 API'sini jsonv1.DefaultOptionsV1() ile çağırmanın birebir v1 çıktısı verdiğini gösteriyor. Dördüncü satırda yalnızca nil dilim davranışını v1'e çektik: Lakaplar yeniden null oldu, ama nil map hâlâ v2 gibi {} yazıldı. Rehber, tersini de önerir: json.Marshal(v, jsonv1.DefaultOptionsV1(), json.FormatNilSliceAsNull(false)) neredeyse tamamen v1 gibi davranır, yalnızca nil dilimleri boş dizi yazar. Bu sayede davranışları teker teker açarak ilerleyebilirsiniz.
Yinelenen anahtarlar, harf duyarlılığı ve geçersiz UTF-8

v2'nin katı varsayılanlarının arkasında güvenlik kaygısı var. Paket belgesinin "Security Considerations" bölümü şu senaryoyu anlatır: biri isteği doğrulayan, diğeri yürüten iki servis aynı JSON'u farklı yorumlarsa saldırgan bir kullanıcının kimlik bilgileriyle doğrulanıp başka bir kullanıcı adına işlem yapabilir. RFC 8259 yinelenen adların nasıl yorumlanacağını tanımlamadığı için farklı uygulamalar farklı sonuç verebilir. v2 bu belirsizlikleri varsayılan olarak hata sayar.
package main
import (
jsonv1 "encoding/json"
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
)
type Kullanici struct {
Ad string `json:"ad"`
Rol string `json:"rol"`
EPost string `json:"ePosta"`
}
func main() {
cift := []byte(`{"ad":"Ali","rol":"okur","rol":"admin"}`)
var u1 Kullanici
fmt.Println(jsonv1.Unmarshal(cift, &u1), u1.Rol)
var u2 Kullanici
fmt.Println(json.Unmarshal(cift, &u2))
var u3 Kullanici
fmt.Println(json.Unmarshal(cift, &u3, jsontext.AllowDuplicateNames(true)), u3.Rol)
harf := []byte(`{"AD":"Zeynep","eposta":"z@ornek.com"}`)
var k1, k2 Kullanici
jsonv1.Unmarshal(harf, &k1)
json.Unmarshal(harf, &k2)
fmt.Printf("v1: %+v\nv2: %+v\n", k1, k2)
var k3 Kullanici
json.Unmarshal(harf, &k3, json.MatchCaseInsensitiveNames(true))
fmt.Printf("v2+opt: %+v\n", k3)
bozuk := []byte("{\"ad\":\"Ay\xffe\"}")
var b1, b2 Kullanici
fmt.Println(jsonv1.Unmarshal(bozuk, &b1), []byte(b1.Ad))
fmt.Println(json.Unmarshal(bozuk, &b2))
}
// Çıktı:
// <nil> admin
// jsontext: duplicate object member name "rol"
// <nil> admin
// v1: {Ad:Zeynep Rol: EPost:z@ornek.com}
// v2: {Ad: Rol: EPost:}
// v2+opt: {Ad:Zeynep Rol: EPost:z@ornek.com}
// <nil> [65 121 239 191 189 101]
// jsontext: invalid UTF-8 within "/ad" after offset 9Çıktıyı satır satır okuyalım. v1 aynı anahtarın iki kopyasını kabul edip sonuncuyu (admin) aldı; v2 jsontext: duplicate object member name "rol" hatası verdi ve AllowDuplicateNames(true) ile v1 davranışına döndü. Harf duyarlılığında v1, "AD" ve "eposta" anahtarlarını ad ve ePosta etiketli alanlara gevşek biçimde eşledi; v2 tam eşleşme aradığı için iki alanı da boş bıraktı ve MatchCaseInsensitiveNames(true) ile doldurdu. Son iki satırda bozuk bir bayt (\xff) içeren girdiyi v1 sessizce Unicode yer değiştirme karakterine (239 191 189 baytları) çevirdi, v2 ise hata verdi. Belge bu değiştirmeyi, metnin anlamını değiştiren bir veri bozulması olarak niteler.
omitempty, omitzero ve time.Duration
v2, omitempty'nin anlamını değiştirdi. v1'de alan Go açısından "boş" bir değerse (false, 0, nil işaretçi, boş dizi, dilim, map ya da string) atlanırdı. v2'de alan, JSON'a yazıldığında boş bir JSON değeri (null, "", {}, []) olacaksa atlanır. Sonuç olarak dilim, map ve string için iki sürüm aynı davranır, ama sayı ve bool alanlarda fark çıkar: 0 boş bir JSON değeri değildir, bu yüzden v2 onu yazar. Belge, bool, sayı, işaretçi ya da arayüz alanlarındaki omitempty kullanımlarının omitzero'ya taşınmasını önerir; omitzero her iki sürümde de aynı çalışır ve varsa tipin IsZero() bool metodunu kullanır (örneğin time.Time).
package main
import (
jsonv1 "encoding/json"
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"os"
"strings"
"time"
)
type Gorev struct {
Ad string `json:"ad"`
Adet int `json:"adet,omitempty"`
Etiket []string `json:"etiket,omitempty"`
Sure time.Duration `json:"sure"`
Bitis time.Time `json:"bitis,omitzero"`
}
type Hedef struct {
Ad string `json:"ad"`
}
func main() {
g := Gorev{Ad: "yedek", Sure: 90 * time.Second}
b1, err := jsonv1.Marshal(g)
fmt.Println(string(b1), err)
_, err = json.Marshal(g)
fmt.Println(err)
b3, err := json.Marshal(g, jsonv1.FormatDurationAsNano(true))
fmt.Println(string(b3), err)
// MarshalWrite doğrudan io.Writer'a yazar.
m := map[string]int{"z": 1, "a": 2, "m": 3}
json.MarshalWrite(os.Stdout, m, json.Deterministic(true), jsontext.Multiline(true))
fmt.Println()
var hedef Hedef
err = json.UnmarshalRead(strings.NewReader(`{"ad":"x","fazla":1}`), &hedef, json.RejectUnknownMembers(true))
fmt.Println(err)
}
// Çıktı:
// {"ad":"yedek","sure":90000000000} <nil>
// json: cannot marshal from Go time.Duration within "/sure": no default representation
// {"ad":"yedek","adet":0,"sure":90000000000} <nil>
// {
// "a": 2,
// "m": 3,
// "z": 1
// }
// json: cannot unmarshal JSON string into Go main.Hedef: unknown object member name "fazla"İlk satır v1 çıktısı: adet 0 olduğu için atlandı, sure nanosaniye sayısı olarak yazıldı, sıfır time.Time değeri omitzero sayesinde hiç görünmedi. İkinci satırda v2 time.Duration için varsayılan gösterim olmadığını söyleyerek hata verdi. Üçüncü satırda FormatDurationAsNano(true) ile süre yeniden sayı oldu, ama bu kez "adet":0 çıktıya girdi; işte omitempty farkı budur. Bu alanda omitzero kullanılsaydı iki sürüm de aynı çıktıyı verirdi. Zaman tipleri için time paketi yazısına bakabilirsiniz.
Yeni API: MarshalWrite, UnmarshalRead ve Options
v2 her işlemi üç biçimde sunar: []byte ile çalışan Marshal/Unmarshal, io.Writer/io.Reader ile çalışan MarshalWrite/UnmarshalRead ve jsontext.Encoder/Decoder ile çalışan MarshalEncode/UnmarshalDecode. Geçiş rehberi, v1'de bir io.Writer'a yazmak için Encoder oluşturmak gerekirken v2'de MarshalWrite'ın bunu tek çağrıda yaptığını örnek verir. UnmarshalRead girdinin tek bir JSON değeri olmasını bekler ve okuyucuyu sonuna kadar tüketir; akıştaki birden fazla değeri sırayla okumak için UnmarshalDecode kullanılır ve değer kalmadığında io.EOF döner.
Yukarıdaki örneğin son iki çıktısı bu fonksiyonları gösteriyor. MarshalWrite map'i doğrudan standart çıktıya yazdı; json.Deterministic(true) anahtarları sıraladı, jsontext.Multiline(true) çıktıyı girintili yaptı. UnmarshalRead ise RejectUnknownMembers(true) verildiği için hedef struct'ta karşılığı olmayan fazla anahtarında durdu. Seçeneklerin bir kısmı yalnızca serileştirmeyi (Deterministic, FormatNilSliceAsNull, OmitZeroStructFields), bir kısmı yalnızca ayrıştırmayı (RejectUnknownMembers) etkiler; ilgisiz seçenekler sessizce yok sayılır. Belgeye göre Deterministic aynı ikili dosyanın farklı çalıştırmaları arasında aynı baytları garanti eder, farklı derlemeler arasında değil.
Struct etiketi seçenekleri
v2 belgesi json etiketinde şu seçenekleri tanımlar: omitzero, omitempty, string, case ve embed. string seçeneği v2'de yalnızca normalde JSON sayısı olarak yazılacak değerlere uygulanabilir ve sayıyı tırnak içinde yazar; belgeye göre bu, 64 bit tamsayıları tam olarak temsil edemeyen JSON ayrıştırıcıları için çoğu zaman gereklidir. Dizi, dilim, struct gibi bileşik tiplere uygulamak hata verir. embed, Go'daki struct gömmenin JSON karşılığıdır. Go 1.27 sürüm notları deney sürecinde inline seçeneğinin adının embed olarak değiştirildiğini, format ve unknown etiket seçeneklerinin kaldırıldığını belirtir. Rehberin hatırlattığı bir başka nokta: Go 1.27 ile gelen yeni etiketler v1 paketinde de desteklenir.
jsontext: token ve değer düzeyinde JSON
encoding/json/jsontext paketi JSON'un söz dizimsel (syntactic) işlenmesinden sorumludur. Bir token; null, true, false, bir string, bir sayı ya da nesne ve dizi başlangıç ve bitiş ayraçlarından biridir. Bir değer ise tam bir JSON birimidir ve jsontext.Value tipinde ham metni tutan bir []byte'tır. Encoder ve Decoder bir durum makinesi tutar, böylece okunan ya da yazılan token dizisinin her zaman geçerli JSON oluşturduğunu denetler. Aşağıda bir nesneyi token token okuyup ardından bir değeri sıkıştırıyoruz.
package main
import (
"encoding/json/jsontext"
"fmt"
"io"
"log"
"strings"
)
func main() {
dec := jsontext.NewDecoder(strings.NewReader(`{"urun":"kalem","fiyat":12.5,"stok":[3,4]}`))
for {
tok, err := dec.ReadToken()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v %s\n", tok.Kind(), tok)
}
v := jsontext.Value(`{ "b" : 2, "a" : [1, 2] }`)
if err := v.Compact(); err != nil {
log.Fatal(err)
}
fmt.Println(v.String())
}
// Çıktı:
// { {
// string urun
// string kalem
// string fiyat
// number 12.5
// string stok
// [ [
// number 3
// number 4
// ] ]
// } }
// {"b":2,"a":[1,2]}İki nokta ve virgül için token yoktur; belge bunların varlığının dilbilgisinden çıkarılabildiğini söyler. Value.Compact gereksiz boşlukları atar, Value.Indent ve Value.Canonicalize ise sırasıyla girintili ve RFC 8785 kurallı (canonical) biçim üretir. Çok büyük girdileri tamamını belleğe almadan işlemek gerektiğinde Decoder ile token düzeyinde çalışmak en esnek yoldur.
Geçiş stratejileri

Geçiş rehberi, riske ne kadar tahammülünüz olduğuna göre üç yaklaşım önerir:
- Hepsi birden: Basit ya da riski düşük uygulamalarda import satırlarını
encoding/json/v2yapın, testleri çalıştırın ve sorun çıkan yerlerde uygun uyumluluk seçeneğini ekleyin. - Seçenek seçenek: Önce tüm çağrıları
json.Marshal(v, jsonv1.DefaultOptionsV1())biçimine taşıyın. Bu değişiklik davranışı değiştirmez. Sonra v1 seçeneklerini teker tekerfalseyaparak v2 davranışlarını açın. - jsonsplit: Üretimdeki sunucular için rehber, v1 ve v2'yi aynı anda çalıştırıp farkları raporlayan ama v1 sonucunu döndüren
github.com/go-json-experiment/jsonsplitpaketini anlatır. Bu üçüncü taraf bir modüldür ve serileştirme maliyetini yaklaşık iki katına çıkarır; çağrıların yalnızca bir kısmını denetleyen bir oran ayarı vardır.
Hangi yolu seçerseniz seçin, rehberin önerisi aynıdır: bir çıktı farkı her zaman bir şeyin bozulduğu anlamına gelmez, ama bozulma ihtimali demektir. Farkları test ve izleme ile görünür kılın, v1 davranışını koruduğunuz yerleri not edin ve geçiş bittikten sonra bunları yeniden değerlendirin. Go 1.27 sürüm notları ayrıca v1 paketinin yeni uygulamasıyla sorun yaşayanların derleme sırasında GOEXPERIMENT=nojsonv2 ile eski uygulamaya dönebileceğini, bu seçeneğin ise ileride kaldırılacağını belirtir. Testleri yazarken testing paketi yazısındaki tablo testleri, v1 ve v2 çıktılarını yan yana karşılaştırmak için uygun bir kalıptır.
Sık yapılan hatalar
Sayı alanlarında omitempty bırakmak
v2'ye geçince "adet":0 gibi alanların çıktıya girmeye başlaması en yaygın sürprizdir. Bool, sayı, işaretçi ve arayüz alanlarındaki omitempty etiketlerini omitzero ile değiştirin; iki sürüm de aynı sonucu verir.
Duration alanlarını unutmak
v2 time.Duration için varsayılan gösterim tanımlamaz ve serileştirme hata verir. Ya alanı v1 uyumlu FormatDurationAsNano seçeneğiyle yazın ya da süreyi kendi seçtiğiniz bir biçimde (örneğin saniye sayısı ya da metin) temsil eden bir tip kullanın.
Hata mesajı metnine bağımlı testler
v2'nin hata mesajları farklıdır ve Go 1.27 notlarına göre v1 paketinin mesaj metni bile değişebilir. Hata tipini errors.AsType[*json.SemanticError] ya da jsontext.SyntacticError ile denetlemek, mesaj karşılaştırmaktan daha dayanıklıdır. Hata tipleri için hata yönetimi yazısına bakın.
Büyük tamsayıları any içine okumak
Belge, iki sürümün de somut tamsayı tiplerinde hassasiyeti koruduğunu, ama bir any değerine ayrıştırırken JSON sayılarını varsayılan olarak float64 ile temsil ettiğini ve büyük tamsayıların hassasiyet kaybedebileceğini söyler. Kimlik numarası gibi alanlarda somut bir tip ya da string etiket seçeneği kullanın.
Sık Sorulan Sorular
Aynı programda v1 ve v2'yi birlikte kullanabilir miyim?
Evet. Geçiş rehberine göre iki paket birbiriyle uyumludur: bir tip v2'nin MarshalerTo arayüzünü uyguluyorsa v1 json.Marshal ile serileştirildiğinde de bu özel yöntem çağrılır. Örneklerdeki gibi farklı import adlarıyla ikisini aynı dosyada kullanabilirsiniz.
v2 daha mı hızlı?
Go 1.27 notları, yeni uygulamada serileştirme performansının genel olarak eskisiyle aynı düzeyde, ayrıştırmanın ise belirgin biçimde daha hızlı olduğunu söyler. v1 paketi de artık aynı uygulamayı kullandığı için bu kazanç ona da yansır.
Yeni kodda hangisini kullanmalıyım?
encoding/json belgesi, yeni JSON kullanımlarının v2 paketini tercih etmesini önerir ve v1 paketinin sonsuza dek destekleneceğini ekler. Yeni bir projeye başlıyorsanız v2'nin katı varsayılanları sizi birçok belirsizlikten korur.
Bilinmeyen alanları nasıl reddederim?
v2'de json.RejectUnknownMembers(true) seçeneğini verin. Varsayılan davranış, iki sürümde de bilinmeyen alanları yok saymaktır; reddedildiğinde hata json.ErrUnknownName'i sarar.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Go öğrenme rehberi · encoding/json kullanımı · Go 1.27 yenilikleri · struct ve alan etiketleri · time paketi · CSV ve JSON dönüşümü