Go'da JSON ile çalışmak için standart kütüphanedeki encoding/json paketini kullanırsınız: json.Marshal(v) bir Go değerini JSON baytlarına çevirir, json.Unmarshal(veri, &v) JSON metnini bir işaretçinin gösterdiği değere doldurur. Hangi alanın hangi anahtarla yazılacağını struct alan etiketleri belirler: json:"ad,omitempty" gibi. Akış halindeki veride json.NewDecoder ve json.NewEncoder bir io.Reader veya io.Writer üzerinde çalışır.
Bu yazı v1 olarak anılan encoding/json paketini anlatıyor: etiket seçenekleri (omitempty, Go 1.24 ile gelen omitzero, string, -), JSON ile Go tipleri arasındaki eşleme, bilinmeyen alanlar, hata tipleri, any ile dinamik veri, json.Number, akış işleme ve özel MarshalJSON metotları. Go 1.27 ile gelen encoding/json/v2 paketi ve iki sürüm arasındaki davranış farkları JSON v2 geçiş yazısında ayrıca ele alınıyor. Örneklerin tümü Go 1.27.3 ile çalıştırıldı; konunun öğrenme yolundaki yeri için Go öğrenme rehberine bakabilirsiniz.
Marshal ve struct etiketleri

Marshal değeri özyinelemeli olarak gezer. Struct'lar JSON nesnesine dönüşür ve yalnızca dışa aktarılmış (büyük harfle başlayan) alanlar yazılır; küçük harfle başlayan alanlar sessizce atlanır. Anahtar olarak varsayılan olarak alan adı kullanılır. Alanın etiketindeki json anahtarı bir ad ve virgülle ayrılmış seçenekler içerir. Struct tanımlamanın ve etiketlerin genel kuralları struct yazısında anlatılıyor; burada JSON'a özgü seçenekleri ele alıyoruz:
| Etiket | Anlamı |
|---|---|
| json:"ad" | alan JSON'da "ad" anahtarıyla yazılır |
| json:"ad,omitempty" | boş değerse (false, 0, nil işaretçi, nil arayüz, uzunluğu 0 olan dizi, dilim, harita, string) alan atlanır |
| json:"ad,omitzero" | tipin sıfır değeriyse ya da IsZero() true dönerse alan atlanır (Go 1.24) |
| json:",omitempty" | ad değişmez, yalnızca seçenek eklenir |
| json:"-" | alan her zaman yok sayılır |
| json:"-," | alan "-" adlı anahtarla yazılır |
| json:"ad,string" | sayı, bool veya string değer JSON string'i içinde yazılır |
package main
import (
"encoding/json"
"fmt"
"log"
"time"
)
type Kullanici struct {
ID int `json:"id"`
Ad string `json:"ad"`
Eposta string `json:"eposta,omitempty"`
Yas int `json:"yas,omitempty"`
Kayit time.Time `json:"kayit,omitzero"`
Sifre string `json:"-"`
Bakiye int64 `json:"bakiye,string"`
Etiketler []string `json:"etiketler"`
notlar string
}
func main() {
k := Kullanici{ID: 7, Ad: "Ayşe", Sifre: "gizli", Bakiye: 1500, notlar: "görünmez"}
b, err := json.Marshal(k)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
k.Eposta = "ayse@example.com"
k.Kayit = time.Date(2026, 11, 7, 9, 30, 0, 0, time.UTC)
k.Etiketler = []string{}
b, _ = json.MarshalIndent(k, "", " ")
fmt.Println(string(b))
}
// Çıktı:
// {"id":7,"ad":"Ayşe","bakiye":"1500","etiketler":null}
// {
// "id": 7,
// "ad": "Ayşe",
// "eposta": "ayse@example.com",
// "kayit": "2026-11-07T09:30:00Z",
// "bakiye": "1500",
// "etiketler": []
// }Çıktıdaki ayrıntılara bakalım. Sifre alanı - etiketi yüzünden, notlar ise dışa aktarılmadığı için hiç görünmez. Bakiye, string seçeneği nedeniyle "1500" olarak tırnak içinde yazıldı. İlk çıktıda Kayit yok: omitzero sıfır değerli time.Time alanını atlar. Go 1.24 sürüm notları bu seçeneği getirirken omitempty seçeneğinden farklı olarak sıfır değerli time.Time değerlerini atladığını ve bunun sık bir sorun kaynağını çözdüğünü özellikle belirtir; omitempty tanımındaki "boş" değerler listesinde struct tipleri yer almaz. İkisi birlikte yazılırsa alan, boş veya sıfır olduğunda atlanır.
Etiketler alanındaki fark da önemlidir: nil dilim null, boş ama nil olmayan dilim [] olarak yazılır. Bir API'nin istemcileri null yerine boş dizi bekliyorsa dilimi []string{} ile başlatmanız gerekir. Zaman değerleri time.Time tipinin kendi MarshalJSON metodu sayesinde RFC 3339 biçiminde yazılır; zaman biçimleri time paketi yazısında anlatılıyor.
Hangi Go tipi hangi JSON değerine dönüşür?
Marshal belgesindeki kurallar özetle şöyledir: bool değerler JSON boolean'a, tamsayı ve ondalıklı sayılar JSON sayısına, string'ler geçerli UTF-8'e zorlanmış JSON string'ine dönüşür. Dilim ve diziler JSON dizisi olur, tek istisna []byte tipidir: base64 ile kodlanmış bir string yazılır. Haritaların anahtar tipi string, tamsayı ya da encoding.TextMarshaler uygulayan bir tip olmalıdır ve anahtarlar sıralanarak yazılır. İşaretçi gösterdiği değer gibi yazılır; nil işaretçi ve nil arayüz null olur. Kanal, karmaşık sayı ve fonksiyon değerleri JSON'da gösterilemez; NaN ve sonsuz değerler de hata üretir. Döngüsel veri yapıları da desteklenmez.
package main
import (
"encoding/json"
"fmt"
)
type Sepet struct {
Urunler []string `json:"urunler"`
Ozellik map[string]int `json:"ozellik"`
Kupon *string `json:"kupon"`
}
func main() {
var bos Sepet
b, _ := json.Marshal(bos)
fmt.Println(string(b))
dolu := Sepet{Urunler: []string{}, Ozellik: map[string]int{"z": 1, "a": 2, "m": 3}}
b, _ = json.Marshal(dolu)
fmt.Println(string(b))
b, _ = json.Marshal([]byte("Go"))
fmt.Println(string(b))
_, err := json.Marshal(map[string]any{"f": func() {}})
fmt.Println(err)
fmt.Println(json.Valid([]byte(`{"a":1}`)), json.Valid([]byte(`{a:1}`)))
}
// Çıktı:
// {"urunler":null,"ozellik":null,"kupon":null}
// {"urunler":[],"ozellik":{"a":2,"m":3,"z":1},"kupon":null}
// "R28="
// json: unsupported type: func()
// true falseVarsayılan olarak Marshal, HTML içine güvenle gömülebilmesi için string'lerdeki <, > ve & karakterlerini \u003c, \u003e ve \u0026 olarak kaçışlar. Bu davranışı bir Encoder üzerinde SetEscapeHTML(false) çağırarak kapatabilirsiniz; aşağıdaki akış örneğinde ikisini yan yana göreceksiniz. json.Valid ise bir bayt diliminin geçerli JSON olup olmadığını, sonucu bir Go değerine yerleştirmeden kontrol eder.
Unmarshal: JSON'dan Go değerine

Unmarshal ikinci argüman olarak bir işaretçi ister; nil veya işaretçi olmayan bir değer verirseniz InvalidUnmarshalError döner. Struct'a çözerken gelen anahtarlar alan adı ya da etiketle büyük-küçük harf ayrımı yapılmadan eşleştirilir; birden fazla alan eşleşirse birebir eşleşen tercih edilir. Struct'ta karşılığı olmayan anahtarlar sessizce yok sayılır, JSON'da bulunmayan alanlar ise dokunulmadan kalır. Bu son kural, aynı değişkeni tekrar kullandığınızda önceki çözümün değerlerinin korunması anlamına gelir:
package main
import (
"encoding/json"
"errors"
"fmt"
)
type Siparis struct {
No int `json:"no"`
Tutar float64 `json:"tutar"`
Durum string `json:"durum"`
Notlar string `json:"notlar"`
}
func main() {
veri := []byte(`{"NO": 12, "tutar": 99.9, "durum": "kargoda", "bilinmeyen": true}`)
var s Siparis
err := json.Unmarshal(veri, &s)
fmt.Printf("%+v %v\n", s, err)
err = json.Unmarshal([]byte(`{"no": "on iki", "durum": "iptal"}`), &s)
fmt.Printf("%+v\n", s)
fmt.Println(err)
var ute *json.UnmarshalTypeError
if errors.As(err, &ute) {
fmt.Println("alan:", ute.Field, "| JSON:", ute.Value, "| Go tipi:", ute.Type)
}
err = json.Unmarshal([]byte(`{"no": 1,}`), &s)
var se *json.SyntaxError
fmt.Println(errors.As(err, &se), err)
err = json.Unmarshal(veri, s)
fmt.Println(err)
}
// Çıktı:
// {No:12 Tutar:99.9 Durum:kargoda Notlar:} <nil>
// {No:12 Tutar:99.9 Durum:iptal Notlar:}
// json: cannot unmarshal string into Go struct field Siparis.no of type int
// alan: no | JSON: string | Go tipi: int
// true invalid character '}' looking for beginning of object key string
// json: Unmarshal(non-pointer main.Siparis)İlk çağrıda "NO" anahtarı no etiketli alanla eşleşti ve bilinmeyen anahtarı hata vermeden atlandı. İkinci çağrıda no alanına string geldiği için tip hatası oluştu; belgeye göre Unmarshal bu durumda sorunlu alanı atlar, çözebildiği kadarını çözmeye devam eder ve ilk tip hatasını *json.UnmarshalTypeError olarak döndürür. Bu yüzden Durum "iptal" oldu, No ve Tutar ise önceki çağrıdan kalan değerlerini korudu. Belge sorunlu alandan sonraki alanların hepsinin çözüleceğinin garanti olmadığını da not eder; hata aldığınızda değişkenin içeriğine güvenmeyin. Hatalı JSON metni *json.SyntaxError üretir.
Yapısı bilinmeyen JSON: any, map ve json.Number
Hedef bir arayüz değeri (any) olduğunda Unmarshal JSON tiplerini sabit Go tiplerine yerleştirir: boolean için bool, sayı için float64, string için string, dizi için []any, nesne için map[string]any, null için nil. Sayıların her zaman float64 olması büyük tamsayılarda hassasiyet kaybına yol açar; paket belgesi de güvenlik notlarında büyük JSON tamsayılarının ondalıklı tiplere çözülürken hassasiyet kaybedeceğini yazar. Bunu önlemek için Decoder.UseNumber sayıları json.Number (sayının metnini tutan bir string tipi) olarak saklar:
package main
import (
"encoding/json"
"fmt"
"strings"
)
func main() {
veri := `{"ad": "Kaan", "puan": 12345678901234567, "aktif": true, "diller": ["go", "sql"], "adres": null}`
var m map[string]any
if err := json.Unmarshal([]byte(veri), &m); err != nil {
panic(err)
}
for _, k := range []string{"ad", "puan", "aktif", "diller", "adres"} {
fmt.Printf("%-7s %T %v\n", k, m[k], m[k])
}
dec := json.NewDecoder(strings.NewReader(veri))
dec.UseNumber()
var m2 map[string]any
if err := dec.Decode(&m2); err != nil {
panic(err)
}
n := m2["puan"].(json.Number)
i, err := n.Int64()
fmt.Printf("%T %v %d %v\n", n, n, i, err)
}
// Çıktı:
// ad string Kaan
// puan float64 1.2345678901234568e+16
// aktif bool true
// diller []interface {} [go sql]
// adres <nil> <nil>
// json.Number 12345678901234567 12345678901234567 <nil>12345678901234567 sayısı float64 tipine çözülünce son hanesi değişti; json.Number ile Int64() çağrısı ise tam değeri verdi. Haritadan değer okurken bir tip iddiası (m2["puan"].(json.Number)) gerekir; tip iddiası ve tip switch type assertion yazısında anlatılıyor. Yapısı bilinen verilerde map[string]any yerine bir struct tanımlamak hem daha güvenli hem de daha okunaklıdır, çünkü tip hataları çözme sırasında yakalanır.
Akış işleme: Decoder ve Encoder
json.NewDecoder(r) bir io.Reader kaynağından art arda JSON değerleri okur. HTTP isteği gövdesi, dosya ya da satır başına bir nesne içeren log akışı için uygundur. Decode her çağrıda sıradaki değeri çözer ve veri bittiğinde io.EOF döndürür. DisallowUnknownFields çağrılmışsa hedef struct'ta karşılığı olmayan anahtarlar hata üretir; bu, istemcinin yanlış yazdığı alanları sessizce kaybetmemek için kullanışlıdır. json.NewEncoder(w) ise her Encode çağrısında değeri yazar ve sonuna satır sonu ekler; SetIndent girintiyi, SetEscapeHTML HTML kaçışını ayarlar.
package main
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
)
type Olay struct {
Tip string `json:"tip"`
Deger int `json:"deger"`
}
func main() {
akis := `{"tip": "tik", "deger": 1}
{"tip": "kaydir", "deger": 40}
{"tip": "tik", "deger": 2}`
dec := json.NewDecoder(strings.NewReader(akis))
for {
var o Olay
err := dec.Decode(&o)
if err == io.EOF {
break
}
if err != nil {
fmt.Println("hata:", err)
return
}
fmt.Printf("%s=%d\n", o.Tip, o.Deger)
}
kati := json.NewDecoder(strings.NewReader(`{"tip": "tik", "deger": 1, "fazla": 0}`))
kati.DisallowUnknownFields()
var o Olay
fmt.Println(kati.Decode(&o))
enc := json.NewEncoder(os.Stdout)
enc.Encode(map[string]string{"html": "<b>Kaşar & Beyaz</b>"})
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(map[string]string{"html": "<b>Kaşar & Beyaz</b>"})
}
// Çıktı:
// tik=1
// kaydir=40
// tik=2
// json: unknown field "fazla"
// {"html":"\u003cb\u003eKaşar \u0026 Beyaz\u003c/b\u003e"}
// {
// "html": "<b>Kaşar & Beyaz</b>"
// }HTTP sunucularında istek gövdesini json.NewDecoder(r.Body).Decode(&v) ile okuyup yanıtı json.NewEncoder(w).Encode(v) ile yazmak yaygın bir kalıptır; bunun çalışan örneği net/http web sunucusu yazısında var. Dışarıdan gelen gövdenin boyutunu sınırlamak için okuyucuyu io.LimitReader gibi bir sarmalayıcıdan geçirebilirsiniz; io arayüzleri dosya okuma ve yazma yazısında anlatılıyor.
Özel biçimler: MarshalJSON, UnmarshalJSON ve RawMessage
Bir tip json.Marshaler arayüzünü (MarshalJSON() ([]byte, error)) uyguluyorsa ve nil işaretçi değilse Marshal onun metodunu çağırır. Bu metot yoksa ama tip encoding.TextMarshaler uyguluyorsa sonuç JSON string'i olarak yazılır. Çözme tarafında UnmarshalJSON metodu, girdi null olsa bile çağrılır. json.RawMessage ise ham JSON baytlarını olduğu gibi saklar; bir zarfın tur alanına bakıp içeriği sonra uygun tipe çözmek için kullanılır:
package main
import (
"encoding/json"
"fmt"
"strings"
)
type Seviye int
const (
Dusuk Seviye = iota
Yuksek
)
func (s Seviye) MarshalJSON() ([]byte, error) {
if s == Yuksek {
return []byte(`"yüksek"`), nil
}
return []byte(`"düşük"`), nil
}
func (s *Seviye) UnmarshalJSON(b []byte) error {
var metin string
if err := json.Unmarshal(b, &metin); err != nil {
return err
}
switch strings.ToLower(metin) {
case "yüksek":
*s = Yuksek
case "düşük":
*s = Dusuk
default:
return fmt.Errorf("bilinmeyen seviye %q", metin)
}
return nil
}
type Mesaj struct {
Tur string `json:"tur"`
Veri json.RawMessage `json:"veri"`
}
type Alarm struct {
Seviye Seviye `json:"seviye"`
}
func main() {
b, _ := json.Marshal(Alarm{Seviye: Yuksek})
fmt.Println(string(b))
var m Mesaj
json.Unmarshal([]byte(`{"tur": "alarm", "veri": {"seviye": "düşük"}}`), &m)
fmt.Println(m.Tur, string(m.Veri))
if m.Tur == "alarm" {
var a Alarm
err := json.Unmarshal(m.Veri, &a)
fmt.Println(a.Seviye == Dusuk, err)
}
var a Alarm
fmt.Println(json.Unmarshal([]byte(`{"seviye": "orta"}`), &a))
}
// Çıktı:
// {"seviye":"yüksek"}
// alarm {"seviye": "düşük"}
// true <nil>
// bilinmeyen seviye "orta"MarshalJSON değer alıcıyla, UnmarshalJSON işaretçi alıcıyla tanımlandı; çözme metodunun alıcıyı değiştirebilmesi için işaretçi gerekir. Alıcı tiplerinin farkı metotlar ve alıcı tipleri yazısında anlatılıyor. MarshalJSON metodunun döndürdüğü baytlar geçerli JSON olmalıdır; burada tırnaklı bir string döndürdük. RawMessage alanı, çözme sırasında içeriği ayrıştırmadan sakladığı için tur değerine göre farklı struct tiplerine yönlendirme yapmanızı sağlar.
Sık yapılan hatalar

- Alanları küçük harfle tanımlayıp JSON'da görünmelerini beklemek.
encoding/jsonyalnızca dışa aktarılmış alanları okur ve yazar. Unmarshalçağrısına işaretçi yerine değer vermek. Sonuçjson: Unmarshal(non-pointer ...)hatasıdır.time.Timegibi struct tipli alanlardaomitemptykullanıp atlanmasını beklemek. Go 1.24 ve sonrasındaomitzerokullanın.- Büyük kimlik numaralarını
map[string]anyile çözmek. Sayılarfloat64olur ve hassasiyet kaybolur; struct alanınıint64yapın ya daUseNumberkullanın. - Hata aldıktan sonra yarım dolmuş struct'ı kullanmak. Belge, sorunlu alandan sonraki alanların çözüleceğini garanti etmez.
- Aynı değişkene tekrar tekrar
Unmarshalyapıp eski değerlerin silineceğini sanmak. JSON'da bulunmayan alanlar ve haritadaki eski girdiler korunur; her çözüm için yeni bir değişken kullanın. - Hata mesajı metnine göre karar vermek. Go 1.27'deki yeni uygulamayla metinler değişebilir;
errors.Asile*json.SyntaxErrorveya*json.UnmarshalTypeErrorkontrol edin.
Sık Sorulan Sorular
Bilinmeyen JSON alanlarında nasıl hata alırım?
json.Unmarshal bu seçeneği sunmaz. Bir json.Decoder oluşturup DisallowUnknownFields() çağırın; struct'ta karşılığı olmayan anahtar geldiğinde json: unknown field "fazla" biçiminde hata alırsınız.
JSON çıktısını girintili nasıl yazarım?
Bayt dilimi için json.MarshalIndent(v, "", " "), akış için enc.SetIndent("", " ") kullanın. Birinci argüman her satırın başına eklenecek öneki, ikincisi her girinti seviyesi için tekrarlanan metni belirtir.
encoding/json yerine encoding/json/v2 paketine geçmeli miyim?
Go 1.27 sürüm notlarına göre v1 API desteklenmeye devam edecek ve geçiş zorunlu değil. Paket belgesi ise v1'in tarihsel nedenlerle daha az güvenli varsayılanlarla çalıştığını belirtir ve yeni kullanımlar için v2'yi önerir. v2 örneğin geçersiz UTF-8'i ve tekrar eden anahtarları reddeder, struct alanlarını büyük-küçük harfe duyarlı eşler. Ayrıntılar JSON v2 geçiş yazısında.
JSON'daki null bir alanı nasıl ayırt ederim?
Belgeye göre JSON null değeri arayüz, harita, işaretçi veya dilime çözülürken o değeri nil yapar; diğer tiplerde değere dokunmaz ve hata üretmez. Alanın "hiç gelmedi" ile "null geldi" durumunu ayırmanız gerekiyorsa json.RawMessage tipli bir alan kullanabilirsiniz: anahtar hiç gelmezse alan nil kalır, null gelirse ham baytlar null metnini içerir.
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/v2 geçişi · Go struct yapıları · net/http web sunucusu · Go HTTP istemcisi · time paketi