JSON handling in Go is een van de eerste dingen die je tegenkomt zodra je een HTTP API bouwt of data uitwisselt met een andere service. Gelukkig heeft Go een uitstekend ingebouwd package: encoding/json. Je hoeft geen externe library te installeren en de API is verrassend krachtig zodra je de conventies doorhebt.
In deze handleiding leer je hoe je JSON encodeert en decodeert, hoe struct tags werken, hoe je met streams omgaat en welke valkuilen je beter kunt vermijden. We bouwen voort op concepten uit eerdere artikelen zoals structs en interfaces en functions en error handling.
Waarom JSON in Go zo prettig werkt
Go is een getypeerde taal, terwijl JSON juist losjes en dynamisch is. Toch matcht het vrijwel naadloos: het encoding/json package uit de standard library mapt JSON-objecten direct op je structs via reflectie en struct tags.
Dit betekent dat je geen boilerplate schrijft voor elke API-call. Een struct definiëren is genoeg, de rest gebeurt automatisch bij Marshal en Unmarshal.
De performance is ook prima voor de meeste use cases. Pas bij extreme throughput kijk je naar alternatieven zoals jsoniter of easyjson.
Basis: Marshal en Unmarshal
De twee centrale functies zijn json.Marshal en json.Unmarshal. Marshal zet een Go-waarde om naar JSON-bytes, Unmarshal leest JSON-bytes in een Go-variabele.
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string
Email string
Age int
}
func main() {
u := User{Name: "Sanne", Email: "[email protected]", Age: 29}
data, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(data))
// {"Name":"Sanne","Email":"[email protected]","Age":29}
}
Merk op dat de veldnamen met een hoofdletter beginnen in de output. Dat komt omdat Go alleen geëxporteerde velden (hoofdletter) serialiseert. Onderstreepte of lowercase velden worden genegeerd.
Unmarshal: JSON terug naar een struct
raw := []byte(`{"Name":"Sanne","Email":"[email protected]","Age":29}`)
var u User
if err := json.Unmarshal(raw, &u); err != nil {
panic(err)
}
fmt.Println(u.Name) // Sanne
Let op het &u, Unmarshal heeft een pointer nodig om de waarde te kunnen vullen. Vergeet je dat, dan krijg je een runtime error.
Struct tags: de echte kracht
In de praktijk wil je natuurlijk "name" in plaats van "Name" in je JSON. Dat regel je met struct tags.
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age,omitempty"`
}
De tag is een string na het veldtype, tussen backticks. De json: prefix vertelt het package hoe het veld moet heten. omitempty zorgt ervoor dat het veld uit de output verdwijnt als het een zero-value heeft (0, "", nil, false).
Andere handige tagopties
json:"-", sla dit veld volledig over, ook bij decodenjson:",omitempty", behoud de Go-naam, maar laat lege waardes wegjson:"password,omitempty", hernoemen én omitempty combinerenjson:",string", forceer numerieke waardes als JSON-string (handig voor grote IDs)
type Product struct {
ID int64 `json:"id,string"`
Name string `json:"name"`
Price float64 `json:"price"`
Internal string `json:"-"`
}
Het Internal veld wordt nooit geserialiseerd, waardoor je gevoelige data binnenshuis houdt.
Werken met onbekende of dynamische data
Niet alle JSON heeft een vaste structuur. Soms weet je van tevoren niet welke velden er komen. Daarvoor heb je een paar opties.
map[string]any
var payload map[string]any
json.Unmarshal(raw, &payload)
fmt.Println(payload["name"])
Handig voor snel prototypen, maar je verliest type safety. Elke waarde is any en je moet type asserten voor je er iets mee kunt.
json.RawMessage
Met json.RawMessage kun je een deel van de JSON uitstellen. Dat is ideaal als één veld verschillende structuren kan bevatten afhankelijk van een type-discriminator.
type Event struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
Je leest eerst Type, en beslist dan op basis daarvan hoe je Data verder parst.
JSON streamen met Encoder en Decoder
Voor grotere payloads of netwerkstromen is het efficiënter om te streamen in plaats van alles in één keer in geheugen te laden. Hier komen json.Encoder en json.Decoder in beeld.
func handler(w http.ResponseWriter, r *http.Request) {
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(u)
}
Deze aanpak werkt direct op io.Reader en io.Writer, wat perfect past bij HTTP handlers. Zie ook onze handleiding over HTTP servers bouwen in Go voor de bredere context van request handling.
Strikte validatie met DisallowUnknownFields
Standaard negeert Decoder velden die niet in je struct staan. Soms wil je juist een fout als een client onbekende velden meestuurt:
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&u); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
Dit vangt typos in request bodies vroeg af en voorkomt dat clients stilletjes de verkeerde API gebruiken.
Pretty printing en custom marshaling
Voor logs of debugging wil je soms ingesprongen JSON:
data, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(data))
En als een struct zich anders moet gedragen dan de default serialisatie, implementeer dan de Marshaler of Unmarshaler interface. Denk bijvoorbeeld aan een custom datum-formaat:
type Date struct {
time.Time
}
func (d Date) MarshalJSON() ([]byte, error) {
return []byte(`"` + d.Format("2006-01-02") + `"`), nil
}
Zo stuur je "2026-08-09" in plaats van het hele RFC3339 formaat.
Veelgemaakte fouten bij JSON in Go
Een paar klassiekers die je tijd kunnen besparen:
- Lowercase velden:
name stringwordt nooit geserialiseerd. Maak hetName string. - Pointer vergeten bij Unmarshal:
json.Unmarshal(raw, u)werkt niet, het moet&uzijn. - time.Time formaten: standaard krijg je RFC3339. Wijk je daarvan af, implementeer dan custom marshaling.
- Numerieke precisie: grote
int64waardes verliezen precisie in JavaScript-clients. Gebruik,stringtag om ze als string te sturen. - nil maps en slices: een nil map serialiseert als
null, niet als{}. Initialiseer metmake()als je een leeg object wilt. - Error negeren: controleer altijd de error van Marshal en Unmarshal. Dit sluit aan bij idiomatische error handling in Go.
Performance en concurrency
Voor de meeste web-applicaties is encoding/json snel genoeg. Gebruik je Go in een high-throughput API, combineer dan JSON met goroutines en concurrency om requests parallel te verwerken.
Let op: de Encoder en Decoder zijn niet safe voor concurrent gebruik vanuit meerdere goroutines. Maak per goroutine een eigen instance, of gebruik Marshal/Unmarshal direct, die zijn wel thread-safe.
Voor extreme performance-eisen kun je kijken naar jsoniter, een drop-in replacement die meetbaar sneller is, vooral voor grote payloads.
Praktische tips voor API's
Een paar best practices uit de praktijk:
- Definieer altijd expliciete request- en response-structs. Geen
map[string]anyin publieke handlers. - Gebruik
omitemptybewust, soms wil je juist explicietnullsturen. - Valideer altijd na Unmarshal: de JSON kan technisch correct zijn maar semantisch onzin.
- Log bij een parse-error de ruwe body (zonder gevoelige data) voor debugging.
- Houd response-structs stabiel: clients bouwen erop. Versioneer je API bij breaking changes.
Veelgestelde vragen
Welk package gebruik je voor JSON in Go?
Go heeft een ingebouwd package encoding/json in de standard library. Dit package biedt alles wat je nodig hebt om JSON te encoden en decoden, inclusief streaming via json.Encoder en json.Decoder.
Wat is het verschil tussen Marshal en Unmarshal?
Marshal zet een Go-waarde om naar JSON-bytes, Unmarshal doet het omgekeerde: JSON-bytes terug naar een Go-struct. Marshal gebruik je voor output, Unmarshal voor het lezen van inkomende JSON.
Hoe geef je een veldnaam aan in JSON?
Via struct tags. Zet achter het veld een tag zoals `json:"naam"`. Je kunt ook opties toevoegen zoals omitempty om lege waardes weg te laten of - om een veld helemaal over te slaan.
Hoe ga je om met onbekende JSON-velden?
Gebruik map[string]any voor volledig dynamische data, of json.RawMessage om delen pas later te parsen. Voor strikte validatie kun je Decoder.DisallowUnknownFields inschakelen zodat onbekende velden een error geven.
Moet ik altijd fouten controleren bij JSON handling?
Ja, altijd. Zowel Marshal als Unmarshal kunnen falen bij ongeldige data of verkeerde types. Negeer nooit de returnwaarde van de error. Dit is een vaste regel in idiomatische Go code.
Tot slot
JSON handling in Go voelt na een paar uur oefenen heel natuurlijk. De combinatie van struct tags, encoding/json en de streaming API's dekt vrijwel elke use case die je in productie tegenkomt. Begin met simpele Marshal en Unmarshal, bouw vertrouwd met tags, en voeg pas streaming en custom marshaling toe wanneer je workload erom vraagt.
Wil je verder bouwen? Combineer JSON met channels voor een pipeline die inkomende berichten parst en verwerkt, of lees waarom Go populair is voor backends om te zien waar deze tooling in het grotere plaatje past.