Go JSON Serialization Guide

Go ~4 min read
GoJSONSerializationencoding/jsonPerformance

Master the Go encoding/json package, including struct tag definitions, custom Marshaler/Unmarshaler interfaces, zero-value omission strategies, and performance tuning.

Struct Tag Basics

Go’s encoding/json package uses struct tags to control JSON field mapping. This is the primary mechanism for customizing serialization behavior.

type User struct {
    Name      string    `json:"name"`
    Age       int       `json:"age"`
    Email     string    `json:"email,omitempty"`
    Password  string    `json:"-"` // Always excluded
    CreatedAt time.Time `json:"created_at"`
}

Common Tag Options

TagEffect
json:"name"Map to “name” key
json:"name,omitempty"Omit if zero value
json:"-"Always exclude
json:",string"Encode as string

Marshal and Unmarshal

// Serialize
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
data, err := json.Marshal(user)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))
// {"name":"Alice","age":30,"email":"alice@example.com","created_at":"0001-01-01T00:00:00Z"}

// Pretty print
data, err = json.MarshalIndent(user, "", "  ")

// Deserialize
var parsed User
err = json.Unmarshal(data, &parsed)

Interface-Based Custom Serialization

Implement json.Marshaler and json.Unmarshaler interfaces for full control.

type CustomTime struct {
    time.Time
}

func (ct CustomTime) MarshalJSON() ([]byte, error) {
    return []byte(`"` + ct.Format("2006-01-02") + `"`), nil
}

func (ct *CustomTime) UnmarshalJSON(data []byte) error {
    str := strings.Trim(string(data), `"`)
    t, err := time.Parse("2006-01-02", str)
    if err != nil {
        return err
    }
    ct.Time = t
    return nil
}

Streaming with Encoder/Decoder

For large data or network I/O, use the streaming API.

// Encoder: writes directly to io.Writer
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "  ")
err := enc.Encode(user)

// Decoder: reads directly from io.Reader
dec := json.NewDecoder(resp.Body)
dec.DisallowUnknownFields() // Strict mode
var result User
err = dec.Decode(&result)

Handling Unknown Fields

// Strict mode: reject unknown fields
dec := json.NewDecoder(reader)
dec.DisallowUnknownFields()

// Flexible mode: two-pass approach using map and json.RawMessage
// Pass 1: Capture all fields into a map
var raw map[string]json.RawMessage
json.Unmarshal(data, &raw)

// Pass 2: Extract known fields
var name string
json.Unmarshal(raw["name"], &name)

// Handle remaining unknown keys
delete(raw, "name")
for key, value := range raw {
    fmt.Printf("Unknown field %s: %s\n", key, string(value))
}

Empty Values and omitempty

Understanding Go’s zero values is crucial for omitempty:

// Zero values that are omitted:
// int:     0
// float:   0.0
// bool:    false
// string:  ""
// pointer: nil
// slice:   nil (NOT empty slice!)
// map:     nil (NOT empty map!)
// struct:  (never omitted, even if all fields are zero)

type Example struct {
    Name   string   `json:"name,omitempty"`
    Tags   []string `json:"tags,omitempty"`
    Config *Config  `json:"config,omitempty"`
}

// Empty slice is NOT omitted:
e := Example{Name: "test", Tags: []string{}}
// Tags will be [] not omitted — use pointer for true omission

omitzero (Go 1.24+)

Go 1.24 introduced the omitzero tag option, which omits a field when its value is the zero value for its type. Unlike omitempty, omitzero works reliably with structs — a struct is omitted when all its fields are zero values.

type User struct {
    Name    string `json:"name"`
    Age     int    `json:"age,omitempty"`
    Address Address `json:"address,omitempty"`   // Never omitted (struct)
    Bio     Address `json:"bio,omitzero"`        // Omitted when zero-valued struct
}

// omitzero also works with maps, slices, and pointers,
// behaving identically to omitempty for those types.

omitzero (Go 1.24+)

Go 1.24 introduced the omitempty alternative that works with structs:

type Config struct {
    Debug   bool   `json:"debug,omitempty"`
    Options Options `json:"options,omitzero"` // Omitted if zero value
}

Performance Optimization

// 1. Use json.RawMessage to defer parsing
var raw json.RawMessage
json.Unmarshal(data, &raw)
// Parse specific fields later

// 2. Use sync.Pool for frequently allocated objects
var userPool = sync.Pool{
    New: func() any { return new(User) },
}

// 3. Consider alternative libraries
// - bytedance/sonic: SIMD-accelerated, drop-in replacement
// - mailru/easyjson: code generation for zero-reflection
// - goccy/go-json: fast, compatible API

Common Pitfalls

Unexported Fields

type User struct {
    name string // lowercase = unexported, WON'T be serialized
    Name string // uppercase = exported, WILL be serialized
}

Pointer vs Value Receiver

// MarshalJSON with value receiver works for both
func (ct CustomTime) MarshalJSON() ([]byte, error) { ... }

// MarshalJSON with pointer receiver only works for pointers
func (ct *CustomTime) MarshalJSON() ([]byte, error) { ... }

Number Precision

// JavaScript's Number.MAX_SAFE_INTEGER
data := map[string]any{
    "id": 9007199254740993, // May lose precision
}

// Solution: use json.Number or string
dec := json.NewDecoder(reader)
dec.UseNumber()

Error Handling

var syntaxErr *json.SyntaxError
var unmarshalErr *json.UnmarshalTypeError

if errors.As(err, &syntaxErr) {
    fmt.Printf("Syntax error at byte offset %d\n", syntaxErr.Offset)
} else if errors.As(err, &unmarshalErr) {
    fmt.Printf("Type mismatch: cannot unmarshal %s into Go value of type %s\n",
        unmarshalErr.Value, unmarshalErr.Type)
}

json.RawMessage Advanced Usage

Defer parsing of partial JSON data:

type Event struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"`
}

// Parse event type first, then payload based on type
var event Event
json.Unmarshal(data, &event)

switch event.Type {
case "user_created":
    var user User
    json.Unmarshal(event.Payload, &user)
case "order_placed":
    var order Order
    json.Unmarshal(event.Payload, &order)
}

Generic JSON Handling (Go 1.18+)

Use generics for type-safe JSON operations:

func ParseJSON[T any](data []byte) (T, error) {
    var result T
    err := json.Unmarshal(data, &result)
    return result, err
}

// Usage
user, err := ParseJSON[User](jsonData)
config, err := ParseJSON[Config](configData)

JSON Schema Validation

Using santhosh-tekuri/jsonschema:

import "github.com/santhosh-tekuri/jsonschema/v5"

compiler := jsonschema.NewCompiler()
schema, err := compiler.Compile("schema.json")
if err != nil {
    log.Fatal(err)
}

// Validate expects any, so unmarshal raw bytes first
var data any
json.Unmarshal(jsonBytes, &data)

err = schema.Validate(data)
if err != nil {
    log.Printf("Validation error: %v", err)
}

JSON API Comparison Across Languages

Language Library Serialize Deserialize Pretty Print Config
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize<T>(str) WriteIndented = true JsonSerializerOptions
TypeScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Rust serde_json serde_json::to_string(&obj) serde_json::from_str::<T>(str) serde_json::to_string_pretty(&obj) serde attributes, custom (de)serializers

Frequently Asked Questions (FAQ)

How to handle datetime types in JSON serialization?

Most language JSON libraries do not support datetime types by default. The common approach is to serialize as ISO 8601 format strings (e.g. "2026-07-15T10:30:00Z") or Unix timestamps, then convert back to datetime objects during deserialization.

How to ignore null values or empty fields in JSON serialization?

Different languages have different implementations: Python can filter None values in custom encoders; JavaScript can use replacer functions; Java Jackson uses @JsonInclude annotations; Go uses omitempty struct tags; C# sets DefaultIgnoreCondition.

How to handle circular references in JSON serialization?

Circular references are a common pitfall. Standard JSON libraries usually throw exceptions or cause stack overflow. Solutions include using @JsonIdentityInfo (Java Jackson), implementing custom serializers, or converting to DTOs before serialization.

Why are my private fields not being serialized?

JSON serialization libraries typically only serialize public fields or properties with getters. This is a security consideration. You can use annotations like @JsonProperty (Java Jackson) or [JsonInclude] (C#) to include non-public members.

What are the performance optimization tips for JSON serialization?

1) Reuse serializer instances; 2) Use streaming APIs for large data; 3) Use compile-time generation (C# Source Generator, Go easyjson); 4) Avoid excessive nesting; 5) Use database pagination for large JSON.

Recommended Tools

These tools can help you handle JSON data more efficiently in Go development:

Related Articles