Serialization Basics
10 examples to get you started with Serialization - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to get you started with Serialization - 7 basic and 3 intermediate.
mkdir serdemo && cd serdemo && go mod init example.com/serdemo.main.go (or separate files in one package) and run with go run ..Round-trip a struct through JSON bytes.
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
u := User{ID: 1, Name: "Ada"}
b, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(b))
var decoded User
if err := json.Unmarshal(b, &decoded); err != nil {
panic(err)
}
fmt.Println(decoded.Name)
}Marshal returns []byte; convert with string(b) for printing.Unmarshal needs a pointer to the destination value.Related: Serialization in Go: JSON First, Formats on Demand - why JSON is the default
Tags rename fields and hide internals.
package main
import (
"encoding/json"
"fmt"
)
type Account struct {
Login string `json:"login"`
PasswordHash string `json:"-"`
Role string `json:"role,omitempty"`
}
func main() {
a := Account{Login: "ada", PasswordHash: "secret"}
b, _ := json.Marshal(a)
fmt.Println(string(b))
}json:"login" maps Login to the "login" key.json:"-" excludes PasswordHash from output entirely.omitempty drops zero-value fields like empty Role.Related: Struct Tags for JSON, DB & Validation - multi-tag conventions
Pointers distinguish "missing" from "present zero."
package main
import (
"encoding/json"
"fmt"
)
type Item struct {
Qty int `json:"qty"`
Notes *string `json:"notes,omitempty"`
}
func main() {
empty := Item{Qty: 0}
b1, _ := json.Marshal(empty)
fmt.Println(string(b1))
note := ""
withPtr := Item{Qty: 1, Notes: ¬e}
b2, _ := json.Marshal(withPtr)
fmt.Println(string(b2))
}Qty with value 0 still appears unless you add omitempty.Notes as nil is omitted with omitempty; a pointer to "" is kept.Related: Schema Evolution & Unknown Field Handling - optional fields
Collections encode as JSON arrays and objects.
package main
import (
"encoding/json"
"fmt"
)
func main() {
tags := []string{"go", "json"}
meta := map[string]int{"a": 1, "b": 2}
b1, _ := json.Marshal(tags)
b2, _ := json.Marshal(meta)
fmt.Println(string(b1))
fmt.Println(string(b2))
}nil slices encode as null; empty slices encode as [] when non-nil.encoding.TextMarshaler).map[string]any for stable API contracts.Related: encoding/json Custom Marshaling - typed alternatives to maps
Stream JSON to io.Writer and from io.Reader.
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type Event struct {
Type string `json:"type"`
}
func main() {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
_ = enc.Encode(Event{Type: "click"})
dec := json.NewDecoder(&buf)
var e Event
_ = dec.Decode(&e)
fmt.Println(e.Type)
}Encode appends a newline after each value (JSON Lines friendly).Related: Serialization Best Practices - size limits on bodies
Anonymous embedding flattens JSON output.
package main
import (
"encoding/json"
"fmt"
)
type Timestamps struct {
CreatedAt string `json:"created_at"`
}
type Post struct {
Timestamps
Title string `json:"title"`
}
func main() {
p := Post{Timestamps: Timestamps{CreatedAt: "2026-01-01"}, Title: "Hi"}
b, _ := json.Marshal(p)
fmt.Println(string(b))
}Related: encoding/json Custom Marshaling - embedding with custom marshalers
MarshalIndent pretty-prints during development.
package main
import (
"encoding/json"
"fmt"
)
func main() {
data := map[string]any{"ok": true, "count": 3}
b, _ := json.MarshalIndent(data, "", " ")
fmt.Println(string(b))
}Related: High-Performance JSON: jsoniter & sonic Alternatives - when compact speed matters
Override default RFC3339 time encoding.
package main
import (
"encoding/json"
"fmt"
"time"
)
type DateOnly time.Time
func (d DateOnly) MarshalJSON() ([]byte, error) {
t := time.Time(d)
return json.Marshal(t.Format("2006-01-02"))
}
func main() {
d := DateOnly(time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC))
b, _ := json.Marshal(d)
fmt.Println(string(b))
}time.Time already implements MarshalJSON with RFC3339; new types wrap it for other layouts.MarshalJSON with UnmarshalJSON for symmetric APIs.Related: encoding/json Custom Marshaling - full custom marshaler patterns
Hold unknown or partial JSON until you know the schema.
package main
import (
"encoding/json"
"fmt"
)
type Envelope struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}
func main() {
raw := []byte(`{"type":"user","payload":{"id":1}}`)
var env Envelope
_ = json.Unmarshal(raw, &env)
fmt.Println(env.Type, string(env.Payload))
}RawMessage is a []byte alias that delays nested decode.Type.Related: Schema Evolution & Unknown Field Handling - evolution patterns
Reject requests with unexpected keys.
package main
import (
"bytes"
"encoding/json"
"fmt"
)
type CreateUser struct {
Name string `json:"name"`
}
func main() {
body := []byte(`{"name":"Ada","admin":true}`)
dec := json.NewDecoder(bytes.NewReader(body))
dec.DisallowUnknownFields()
var req CreateUser
err := dec.Decode(&req)
fmt.Println(err)
}Related: Validation with go-playground/validator - post-decode rules
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026