GoNetworking

Go JSON Marshalling and Unmarshalling: Complete Guide

TT
TopicTrick Team
Go JSON Marshalling and Unmarshalling: Complete Guide

Go JSON Marshalling: The Serialization Mirror

Go's encoding/json package converts between Go structs and JSON using reflection and struct tags. Marshalling (json.Marshal or json.NewEncoder) converts a Go struct into a JSON byte slice or stream. Unmarshalling (json.Unmarshal or json.NewDecoder) parses JSON into a Go struct. Struct tags like `json:"field_name,omitempty"` control the mapping between Go field names and JSON keys.

In this module, we will explore how to send and receive JSON data like a professional Go developer.


1. The Serialization Mirror: Reflection Costs

Go's JSON implementation is "Batteries Included" because it uses the reflect package to inspect your types at runtime.

The Inspection Physics

  • The Type Mirror: When you call json.Marshal, Go pauses to inspect the struct's fields, types, and tags. This is a dynamic operation that occurs on every call.
  • The CPU Cost: Reflection-based marshalling is significantly slower than static code generation (like Protobuf or easyjson). Each field lookup requires the CPU to traverse the Go type-header mirror.
  • The Strategy: For 99% of applications, this cost is negligible. However, in high-frequency hardware mirrors (100k+ RPS), developers often switch to Code Generation to bypass reflection and directly map Go types to binary buffers.

2. Working with JSON in Go

  • Unmarshalling: Converting JSON text into a Go Struct (Receiving data).

3. The Field-Tag Mirror: Metadata Physics

Struct tags are the Go compiler's way of allowing Attached Metadata to survive into the binary execution mirror.

The Mapping Physics

  • The String Mirror: Tags like `json:"id"` are stored as raw strings in the binary's symbol table.
  • The Serializer's Path: The JSON engine reads these strings at runtime to decide if a Go field named UserID should be written as "user_id" in the network packet.
  • The Zero-Value Mirror: The omitempty tag guides the physics of "Pruning." If a field's value matches the silicon zero-state (e.g., 0 for int), the serializer skips it entirely, reducing the network payload size.

4. Struct Tags: The Mapping Secret

Go uses "Struct Tags" to define exactly how a field should be represented in JSON. This allows your Go code to use PascalCase while your JSON uses snake_case.

go
type User struct {
    ID        int    `json:"id"`
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
    // Omitting a field from JSON entirely
    Password  string `json:"-"`
}

Marshalling Go to JSON

To send JSON in an HTTP response, you transform your data structure using json.Marshal.

go
func userHandler(w http.ResponseWriter, r *http.Request) {
    u := User{ID: 1, FirstName: "Alice", LastName: "Smith"}

    // Set the Content-Type header so the browser knows this is JSON
    w.Header().Set("Content-Type", "application/json")

    // Encode and write directly to the response
    json.NewEncoder(w).Encode(u)
}

Unmarshalling JSON to Go

When receiving data from an API request, you need to "decode" the incoming JSON body into a Go struct.

go
func createUser(w http.ResponseWriter, r *http.Request) {
    var u User
    
    // Decode the request body into our user struct
    err := json.NewDecoder(r.Body).Decode(&u)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    fmt.Printf("Received user: %s %s\n", u.FirstName, u.LastName)
}

JSON Power Tools

No data available

Performance: Encoders vs. Marshals

Wait! There are two ways to handle JSON in Go. Which one should you use?

Task / Featurejson.Marshaljson.NewEncoder
No comparison data available

Validating Incoming JSON

The encoding/json package does basic type validation during decoding, but it won't enforce business rules (like a required field being non-empty). For production APIs, validate the struct after decoding:

go
type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

func (r *CreateUserRequest) Validate() error {
    if r.Name == "" {
        return errors.New("name is required")
    }
    if r.Email == "" {
        return errors.New("email is required")
    }
    if r.Age < 0 || r.Age > 150 {
        return errors.New("age must be between 0 and 150")
    }
    return nil
}

func createUser(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
    }
    if err := req.Validate(); err != nil {
        http.Error(w, err.Error(), http.StatusUnprocessableEntity)
        return
    }
    // Process valid request...
}

For larger applications, the go-playground/validator package provides struct tag-based validation (like `validate:"required,email"`) that eliminates the boilerplate.


Custom Marshalling with MarshalJSON

Sometimes you need to control exactly how a struct is serialised to JSON. Implement the json.Marshaler interface by defining a MarshalJSON() ([]byte, error) method:

go
type Money struct {
    Amount   int64  // Store as cents internally
    Currency string
}

// Custom marshaller: output as decimal string, not integer
func (m Money) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        Amount   string `json:"amount"`
        Currency string `json:"currency"`
    }{
        Amount:   fmt.Sprintf("%.2f", float64(m.Amount)/100),
        Currency: m.Currency,
    })
}

// Output: {"amount": "42.99", "currency": "USD"}

Similarly, implement UnmarshalJSON([]byte) error for custom parsing logic.


Handling Nullable Fields

Go's zero values (0, "", false) are not the same as JSON null. If you need to distinguish between a field being absent versus present-but-zero, use pointer types:

go
type Profile struct {
    Bio     *string `json:"bio"`       // nil = absent, "" = empty string
    Age     *int    `json:"age"`       // nil = not provided, 0 = zero
    Premium bool    `json:"premium"`   // Can't distinguish false from absent
}

When Bio is nil, it marshals to null in JSON. When it's &"", it marshals to "". This pattern is essential for PATCH endpoints where you need to know which fields the client actually sent.


JSON and the Go Standard Library: A Format Overview

Understanding JSON in the context of other formats Go supports:

FormatPackageUse Case
JSONencoding/jsonREST APIs, config files
XMLencoding/xmlLegacy SOAP services
CSVencoding/csvTabular data export
Gobencoding/gobGo-to-Go binary communication
Protocol Buffersgoogle.golang.org/protobufHigh-performance microservices

For REST APIs serving web and mobile clients, JSON is the clear choice. Protocol Buffers are used in performance-critical microservice-to-microservice communication.


Phase 17: Serialization Architecture Mastery Checklist

  • Verify Field Exportability: Audit all JSON structs. Ensure that fields destined for the wire are Uppercase (exported) to remain visible to the reflection mirror.
  • Audit Tag Consistency: Use a linter (like golangci-lint) to ensure that your struct tags are valid and follow the "snake_case" standard mirror for public APIs.
  • Implement Streaming Decoders: For incoming HTTP requests, use json.NewDecoder(r.Body) to process payloads directly from the network buffer, minimizing RAM allocation spikes.
  • Test Omitempty Semantics: Confirm that "Required Zero-Values" (like a zero balance) are not accidentally pruned by omitempty. Use pointers for true optionality.
  • Use RawMessage for Dynamic Payloads: When handling polymorphic JSON, capture the dynamic segment into json.RawMessage to delay the reflection cost until the subtype is identified.

Read next: Go Web Servers with net/http: The Connectivity Mirror →



Next Steps

JSON is how we talk to clients, but how do we store that data permanently? In our next tutorial, we will explore Database Connectivity, learning how to connect our Go web servers to SQL databases using the standard library and ORMs like GORM.

Common JSON Mistakes in Go

1. Unexported fields are silently ignored json.Marshal skips fields that start with a lowercase letter. Always export fields you want in the JSON output and use struct tags to control the key name: Name string \json:"name"``.

2. Using interface{} for JSON values you know the shape of Unmarshalling into map[string]interface{} works but forces type assertions everywhere downstream. Define a concrete struct — the compiler will catch shape mismatches at build time, not runtime.

3. Forgetting omitempty causes zero values to appear Count int \json:"count"`always serialises, even as"count": 0. Add omitempty (json:"count,omitempty") to omit zero values from the output. Note: omitemptyomits the zero value of the type —false, 0, "", nil`.

4. Using - to skip a field vs omitempty json:"-" always excludes the field. omitempty excludes it only when empty. Use - for fields that should never appear in JSON output (internal state, sensitive data).

5. Time zone information lost in time.Time marshalling time.Time marshals to RFC3339 format, which includes timezone. But if you parse a time string without a timezone offset, it defaults to UTC. Always be explicit about timezones when serialising time values. See the encoding/json package documentation.

Frequently Asked Questions

How do I marshal a Go struct to a JSON string? Use json.Marshal(v) which returns []byte and an error, or json.MarshalIndent(v, "", " ") for pretty-printed output. Convert to string with string(b). Always check the returned error.

How do I handle JSON fields that may or may not be present? Use a pointer type for the field: Name *string \json:"name"`. If the field is absent from the JSON, the pointer will be nil`. If present, it will point to the parsed value.

Can I customise JSON serialisation for a type? Yes — implement the json.Marshaler interface by adding a MarshalJSON() ([]byte, error) method to your type, and json.Unmarshaler by adding UnmarshalJSON([]byte) error. This gives you full control over the JSON representation.