JSON and XML Encoding/Decoding Intermediate¶
Introduction¶
JSON is the lingua franca of web APIs, and Go's encoding/json package makes it straightforward to convert between Go structs and JSON. You'll use json.Marshal/Unmarshal for in-memory conversion and json.Encoder/Decoder for streaming I/O. Struct tags control field naming, omission, and behavior. For XML, encoding/xml follows the same patterns with additional tag syntax for attributes and nesting.
Mastering JSON encoding is essential — virtually every Go web service reads and writes JSON.
Syntax & Usage¶
json.Marshal — Go to JSON¶
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
CreatedAt time.Time `json:"created_at"`
password string // unexported — not included in JSON
}
user := User{
ID: 1,
Name: "Alice",
CreatedAt: time.Now(),
}
data, err := json.Marshal(user)
if err != nil {
return err
}
fmt.Println(string(data))
// {"id":1,"name":"Alice","created_at":"2025-01-15T10:30:00Z"}
// Note: Email omitted because it's empty and has `omitempty`
Pretty-printed output:
json.Unmarshal — JSON to Go¶
jsonStr := `{"id": 1, "name": "Alice", "email": "alice@example.com"}`
var user User
if err := json.Unmarshal([]byte(jsonStr), &user); err != nil {
return fmt.Errorf("parsing user JSON: %w", err)
}
fmt.Printf("%+v\n", user) // {ID:1 Name:Alice Email:alice@example.com ...}
Struct Tags¶
Struct tags control how fields map to JSON keys:
type Product struct {
ID int `json:"id"` // renamed to "id"
Name string `json:"name"` // renamed to "name"
Price float64 `json:"price"` // renamed to "price"
Description string `json:"description,omitempty"` // omit if empty string
InternalRef string `json:"-"` // always excluded
Category string `json:",omitempty"` // keep field name, omit if empty
Count int `json:"count,string"` // encode as JSON string "42"
}
| Tag | Effect |
|---|---|
json:"name" |
Use "name" as JSON key |
json:",omitempty" |
Omit field if zero value |
json:"-" |
Always exclude from JSON |
json:"name,omitempty" |
Rename and omit if empty |
json:",string" |
Encode number/bool as JSON string |
What counts as a zero value for omitempty?
false, 0, "", nil pointer, nil interface, empty array/slice/map. Note: a zero-value struct is not omitted — this is a common surprise.
json.Encoder / json.Decoder — Streaming¶
For HTTP handlers and file I/O, use streaming instead of Marshal/Unmarshal:
// HTTP handler: decode request, encode response
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
user := processRequest(req)
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(user); err != nil {
log.Printf("encoding response: %v", err)
}
}
Reading JSON from a file:
f, err := os.Open("config.json")
if err != nil {
return err
}
defer f.Close()
var config Config
if err := json.NewDecoder(f).Decode(&config); err != nil {
return fmt.Errorf("decoding config: %w", err)
}
Custom MarshalJSON / UnmarshalJSON¶
Implement the json.Marshaler and json.Unmarshaler interfaces for custom serialization:
type Status int
const (
StatusActive Status = iota
StatusInactive
StatusBanned
)
func (s Status) MarshalJSON() ([]byte, error) {
names := map[Status]string{
StatusActive: "active",
StatusInactive: "inactive",
StatusBanned: "banned",
}
name, ok := names[s]
if !ok {
return nil, fmt.Errorf("unknown status: %d", s)
}
return json.Marshal(name) // returns `"active"` (with quotes)
}
func (s *Status) UnmarshalJSON(data []byte) error {
var name string
if err := json.Unmarshal(data, &name); err != nil {
return err
}
values := map[string]Status{
"active": StatusActive,
"inactive": StatusInactive,
"banned": StatusBanned,
}
val, ok := values[name]
if !ok {
return fmt.Errorf("unknown status: %q", name)
}
*s = val
return nil
}
Handling Unknown Fields¶
By default, json.Unmarshal silently ignores unknown fields. To reject them:
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&req); err != nil {
http.Error(w, "unknown field in request", http.StatusBadRequest)
return
}
json.RawMessage — Lazy/Partial Parsing¶
Defer parsing of a JSON field until you know its type:
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"` // raw JSON, parsed later
}
func processEvent(data []byte) error {
var event Event
if err := json.Unmarshal(data, &event); err != nil {
return err
}
switch event.Type {
case "user_created":
var u UserCreatedPayload
if err := json.Unmarshal(event.Payload, &u); err != nil {
return err
}
return handleUserCreated(u)
case "order_placed":
var o OrderPlacedPayload
if err := json.Unmarshal(event.Payload, &o); err != nil {
return err
}
return handleOrderPlaced(o)
default:
return fmt.Errorf("unknown event type: %s", event.Type)
}
}
Encoding Maps and Slices¶
// Map → JSON object
data, _ := json.Marshal(map[string]int{
"alice": 95,
"bob": 87,
})
// {"alice":95,"bob":87}
// Slice → JSON array
data, _ := json.Marshal([]string{"go", "rust", "python"})
// ["go","rust","python"]
// Decoding into a generic structure
var result map[string]any
json.Unmarshal(jsonBytes, &result)
// Numbers become float64, arrays become []any, objects become map[string]any
time.Time in JSON¶
time.Time marshals to RFC 3339 by default ("2025-01-15T10:30:00Z"). For custom formats:
type CustomTime struct {
time.Time
}
const layout = "2006-01-02" // Go's reference time format
func (ct CustomTime) MarshalJSON() ([]byte, error) {
return json.Marshal(ct.Format(layout))
}
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
t, err := time.Parse(layout, s)
if err != nil {
return err
}
ct.Time = t
return nil
}
XML Encoding (Brief)¶
encoding/xml follows the same Marshal/Unmarshal pattern with XML-specific struct tags:
type Person struct {
XMLName xml.Name `xml:"person"`
Name string `xml:"name"`
Age int `xml:"age,attr"` // XML attribute
Address string `xml:"contact>address"` // nested element
}
p := Person{Name: "Alice", Age: 30, Address: "123 Main St"}
data, err := xml.MarshalIndent(p, "", " ")
Output:
Common Pattern: API Response Structs¶
type APIResponse[T any] struct {
Data T `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Status int `json:"status"`
}
func respondJSON[T any](w http.ResponseWriter, status int, data T) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(APIResponse[T]{
Data: data,
Status: status,
})
}
func respondError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(APIResponse[any]{
Error: msg,
Status: status,
})
}
Common Pattern: Config File Parsing¶
type Config struct {
Server ServerConfig `json:"server"`
Database DatabaseConfig `json:"database"`
LogLevel string `json:"log_level"`
}
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
}
type DatabaseConfig struct {
DSN string `json:"dsn"`
MaxOpenConns int `json:"max_open_conns"`
ConnMaxLifetime string `json:"conn_max_lifetime"`
}
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &cfg, nil
}
Quick Reference¶
| Operation | Function | Notes |
|---|---|---|
| Go → JSON bytes | json.Marshal(v) |
Returns []byte, error |
| Go → JSON (pretty) | json.MarshalIndent(v, "", " ") |
Indented output |
| JSON bytes → Go | json.Unmarshal(data, &v) |
Pass pointer to target |
| Stream encode | json.NewEncoder(w).Encode(v) |
Writes to io.Writer |
| Stream decode | json.NewDecoder(r).Decode(&v) |
Reads from io.Reader |
| Rename field | json:"name" |
Struct tag |
| Omit empty | json:",omitempty" |
Skip zero values |
| Exclude field | json:"-" |
Never marshal |
| Lazy parse | json.RawMessage |
Deferred unmarshal |
| Custom format | MarshalJSON() / UnmarshalJSON() |
Implement interfaces |
| Reject unknown | decoder.DisallowUnknownFields() |
Strict parsing |
Best Practices¶
- Always use struct tags — without tags, JSON keys match the Go field name exactly (capitalized), which is non-standard for JSON APIs.
- Use
omitemptythoughtfully — it's great for optional fields but can hide bugs when zero values are meaningful (e.g.,count: 0). - Use
json.Decoderfor HTTP bodies — it's more memory-efficient than reading the body into[]byteand callingUnmarshal. - Set
Content-Type: application/json— always set the header when writing JSON responses. - Define separate request/response structs — don't reuse the same struct for database models and API responses. This prevents accidentally exposing internal fields.
- Use
json.RawMessagefor polymorphic JSON — parse the discriminator field first, then decode the payload into the correct type. - Use pointer fields for nullable JSON —
*stringdistinguishes betweennull/missing (nil) and""(empty string).
Common Pitfalls¶
Unexported fields are invisible to JSON
JSON encoding only sees exported (capitalized) fields. This is the #1 mistake beginners make.omitempty doesn't work on structs
Numbers decode as float64 into any/interface{}
var result map[string]any
json.Unmarshal([]byte(`{"count": 42}`), &result)
count := result["count"] // type is float64, not int!
// To get an int
n := int(result["count"].(float64))
// Or use json.Decoder with UseNumber
dec := json.NewDecoder(r)
dec.UseNumber()
// Numbers become json.Number (a string), parsed explicitly
Marshaling nil slice vs empty slice
var nilSlice []string
emptySlice := []string{}
json.Marshal(nilSlice) // null
json.Marshal(emptySlice) // []
[], not null. Initialize slices explicitly if the distinction matters.
Forgetting to handle Decode errors in HTTP handlers
Performance Considerations¶
json.Marshalvsjson.Encoder:Marshalcreates a[]bytein memory.Encoderwrites directly to anio.Writer. For HTTP responses,Encoderavoids an intermediate buffer allocation.- Struct tag parsing is cached: The
encoding/jsonpackage caches the reflection metadata per type. The first marshal/unmarshal of a type is slower; subsequent calls are fast. - Large JSON payloads: For very large JSON documents, use
json.DecoderwithToken()to stream-parse without loading the entire document into memory. - Alternative libraries:
encoding/jsonuses reflection, which has overhead. For hot paths, consider code-generated alternatives likeeasyjsonorsonicthat can be 5-10x faster. Profile first. json.RawMessageavoids double parsing: When you only need part of a JSON document, useRawMessagefor fields you want to parse later (or not at all).
Interview Tips¶
Interview Tip
"How do you handle JSON in Go?" Use json.Marshal/Unmarshal for in-memory conversion and json.Encoder/Decoder for streaming I/O. Struct tags (json:"name,omitempty") control field naming and behavior. Only exported fields are included. Implement MarshalJSON/UnmarshalJSON for custom serialization.
Interview Tip
"What happens if a JSON field doesn't match any struct field?" By default, it's silently ignored. Use decoder.DisallowUnknownFields() to reject unknown fields. Conversely, struct fields without a matching JSON key get their zero value.
Interview Tip
"How do you handle polymorphic JSON?" Use json.RawMessage to defer parsing. Parse a discriminator field (like "type") first, then unmarshal the raw payload into the correct Go type based on the discriminator value. This is the standard pattern for event systems and APIs with multiple response shapes.
Interview Tip
"What's the difference between a nil slice and an empty slice in JSON?" A nil slice marshals to null, while an initialized empty slice marshals to []. Most APIs expect [] for empty arrays, so initialize your slices explicitly: items := []string{} or items := make([]string, 0).
Key Takeaways¶
json.Marshal/Unmarshalconvert between Go values and JSON bytes;Encoder/Decoderstream to/fromio.Writer/io.Reader.- Struct tags (
json:"name,omitempty") are essential — they control JSON key names, omission, and exclusion. - Only exported fields are visible to the JSON encoder.
- Use
json.RawMessagefor polymorphic JSON and lazy parsing. - Implement
MarshalJSON/UnmarshalJSONfor custom serialization (enums, dates, etc.). - Nil slices marshal to
null, empty slices to[]— be intentional about initialization. - Use
json.Decoderfor HTTP request bodies — it's memory-efficient and supports strict mode.