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 ve struct etiketleri: json:"ad", json:"ad,omitempty", json:"ad,omitzero", json:",omitempty"

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:

EtiketAnlamı
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
go
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.

go
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 false

Varsayı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: 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:

go
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:

go
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.

go
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:

go
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

Sık yapılan hatalar: Alanları küçük harfle tanımlayıp JSON'da görünmelerini beklemek. encoding/json..., Unmarshal çağrısına işaretçi yerine değer vermek. Sonuç json..., time.Time...
  • Alanları küçük harfle tanımlayıp JSON'da görünmelerini beklemek. encoding/json yalnı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.Time gibi struct tipli alanlarda omitempty kullanıp atlanmasını beklemek. Go 1.24 ve sonrasında omitzero kullanın.
  • Büyük kimlik numaralarını map[string]any ile çözmek. Sayılar float64 olur ve hassasiyet kaybolur; struct alanını int64 yapın ya da UseNumber kullanı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 Unmarshal yapı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.As ile *json.SyntaxError veya *json.UnmarshalTypeError kontrol 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