Serialization Basics
10 examples to get you started with Serialization - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir serdemo && cd serdemo && go mod init example.com/serdemo. - Save each snippet as
main.go(or separate files in one package) and run withgo run ..
Basic Examples
1. json.Marshal and json.Unmarshal
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)
}Marshalreturns[]byte; convert withstring(b)for printing.Unmarshalneeds a pointer to the destination value.- Only exported fields participate in JSON encoding.
Related: Serialization in Go: JSON First, Formats on Demand - why JSON is the default
2. Struct tags control JSON keys
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"mapsLoginto the"login"key.json:"-"excludesPasswordHashfrom output entirely.omitemptydrops zero-value fields like emptyRole.
Related: Struct Tags for JSON, DB & Validation - multi-tag conventions
3. Pointers and zero values
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))
}Qtywith value0still appears unless you addomitempty.Notesasnilis omitted withomitempty; a pointer to""is kept.- Use pointers for optional API fields that need explicit null semantics.
Related: Schema Evolution & Unknown Field Handling - optional fields
4. Slices and maps marshal naturally
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))
}nilslices encode asnull; empty slices encode as[]when non-nil.- Map keys must be strings (or types that implement
encoding.TextMarshaler). - Prefer structs over
map[string]anyfor stable API contracts.
Related: encoding/json Custom Marshaling - typed alternatives to maps
5. json.Encoder and json.Decoder for streams
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)
}Encodeappends a newline after each value (JSON Lines friendly).- Decoders suit HTTP bodies without reading everything into memory first.
- Reuse one decoder per body; do not share across requests.
Related: Serialization Best Practices - size limits on bodies
6. Embedded structs promote fields
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))
}- Embedded exported fields promote to the outer struct for encoding.
- Name collisions resolve to the outer field; inner fields shadow on decode carefully.
- Prefer explicit composition when JSON shape should not flatten.
Related: encoding/json Custom Marshaling - embedding with custom marshalers
7. Indent for readable debug output
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))
}- Use indentation only in logs and CLI output, not hot API paths.
- Pretty JSON is larger and slower to generate.
- Production APIs usually return compact JSON.
Related: High-Performance JSON: jsoniter & sonic Alternatives - when compact speed matters
Intermediate Examples
8. Custom time format with MarshalJSON
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.Timealready implementsMarshalJSONwith RFC3339; new types wrap it for other layouts.- Pair
MarshalJSONwithUnmarshalJSONfor symmetric APIs. - Document the wire format in your API spec.
Related: encoding/json Custom Marshaling - full custom marshaler patterns
9. json.RawMessage for deferred parsing
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))
}RawMessageis a[]bytealias that delays nested decode.- Useful for polymorphic events and forward-compatible APIs.
- Validate inner payloads in a second pass after routing on
Type.
Related: Schema Evolution & Unknown Field Handling - evolution patterns
10. DisallowUnknownFields on decode
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)
}- Strict mode catches typos and deprecated fields early.
- Public APIs often stay permissive; internal admin APIs can be strict.
- Combine with validation after a successful decode.
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).