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
| Tag | Effect |
|---|---|
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)
}
Related Resources
- JSON Best Practices — Universal guidelines for all languages
- JSON Schema Validation — Validate your JSON structures
- JSON Security — Protect against common vulnerabilities
- JSON Formatter — Format and validate your JSON online
- JSON Schema Validator — Test your schemas