JSON Handling in Go

JSON (JavaScript Object Notation) is the most common data format for web APIs. Go’s encoding/json package provides comprehensive support for encoding and decoding JSON data.

Basic Structs and JSON

Define Go structs that map to JSON:

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email,omitempty"` // Omit if empty
}

func main() {
    // Create a person
    person := Person{
        Name:  "Alice",
        Age:   30,
        Email: "[email protected]",
    }
    
    // Encode to JSON
    jsonData, err := json.Marshal(person)
    if err != nil {
        fmt.Println("Error encoding JSON:", err)
        return
    }
    
    fmt.Println("JSON:", string(jsonData))
    
    // Decode from JSON
    jsonString := `{"name":"Bob","age":25,"email":"[email protected]"}`
    var decodedPerson Person
    err = json.Unmarshal([]byte(jsonString), &decodedPerson)
    if err != nil {
        fmt.Println("Error decoding JSON:", err)
        return
    }
    
    fmt.Printf("Decoded: %+v\n", decodedPerson)
}

JSON Tags

Control JSON serialization with struct tags:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Password string `json:"-"`        // Never serialize
    Email    string `json:"email,omitempty"`
    Created  string `json:"created_at"`
}

func main() {
    user := User{
        ID:       1,
        Username: "johndoe",
        Password: "secret123",
        Email:    "[email protected]",
        Created:  "2023-01-01",
    }
    
    jsonData, _ := json.MarshalIndent(user, "", "  ")
    fmt.Println(string(jsonData))
}

Output:

{
  "id": 1,
  "username": "johndoe",
  "email": "[email protected]",
  "created_at": "2023-01-01"
}

Nested Structs

Handle complex nested structures:

package main

import (
    "encoding/json"
    "fmt"
)

type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    Country string `json:"country"`
}

type Company struct {
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

type Employee struct {
    Name    string  `json:"name"`
    Age     int     `json:"age"`
    Company Company `json:"company"`
}

func main() {
    employee := Employee{
        Name: "Alice",
        Age:  30,
        Company: Company{
            Name: "Tech Corp",
            Address: Address{
                Street:  "123 Main St",
                City:    "New York",
                Country: "USA",
            },
        },
    }
    
    jsonData, _ := json.MarshalIndent(employee, "", "  ")
    fmt.Println(string(jsonData))
}

Arrays and Slices

Working with JSON arrays:

package main

import (
    "encoding/json"
    "fmt"
)

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func main() {
    // Array of products
    products := []Product{
        {ID: 1, Name: "Laptop", Price: 999.99},
        {ID: 2, Name: "Mouse", Price: 29.99},
        {ID: 3, Name: "Keyboard", Price: 79.99},
    }
    
    // Encode to JSON
    jsonData, err := json.Marshal(products)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    fmt.Println("Products JSON:", string(jsonData))
    
    // Decode from JSON
    jsonString := `[
        {"id": 4, "name": "Monitor", "price": 299.99},
        {"id": 5, "name": "Headphones", "price": 149.99}
    ]`
    
    var decodedProducts []Product
    err = json.Unmarshal([]byte(jsonString), &decodedProducts)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    for _, p := range decodedProducts {
        fmt.Printf("Product: %+v\n", p)
    }
}

Maps

Using maps for flexible JSON structures:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    // Map with string keys
    person := map[string]interface{}{
        "name":  "Charlie",
        "age":   35,
        "email": "[email protected]",
        "skills": []string{"Go", "Python", "JavaScript"},
    }
    
    jsonData, _ := json.MarshalIndent(person, "", "  ")
    fmt.Println("Map JSON:")
    fmt.Println(string(jsonData))
    
    // Decode into map
    jsonString := `{"name":"Diana","department":"Engineering","salary":75000}`
    var employee map[string]interface{}
    
    err := json.Unmarshal([]byte(jsonString), &employee)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    fmt.Println("Name:", employee["name"])
    fmt.Println("Department:", employee["department"])
    fmt.Println("Salary:", employee["salary"])
}

Custom JSON Parsing

Handle dynamic or complex JSON structures:

package main

import (
    "encoding/json"
    "fmt"
)

func parseUnknownJSON(jsonData string) {
    var data interface{}
    err := json.Unmarshal([]byte(jsonData), &data)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    parseValue(data, "")
}

func parseValue(value interface{}, prefix string) {
    switch v := value.(type) {
    case map[string]interface{}:
        for key, val := range v {
            parseValue(val, prefix+key+".")
        }
    case []interface{}:
        for i, val := range v {
            parseValue(val, fmt.Sprintf("%s[%d].", prefix, i))
        }
    default:
        fmt.Printf("%s = %v (%T)\n", prefix[:len(prefix)-1], v, v)
    }
}

func main() {
    jsonString := `{
        "user": {
            "name": "Eve",
            "preferences": {
                "theme": "dark",
                "notifications": true
            },
            "tags": ["golang", "developer"]
        }
    }`
    
    parseUnknownJSON(jsonString)
}

Custom Marshaling

Implement custom JSON marshaling:

package main

import (
    "encoding/json"
    "fmt"
    "strings"
    "time"
)

type Event struct {
    ID          int       `json:"id"`
    Title       string    `json:"title"`
    Description string    `json:"description"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
}

// Custom marshaling for Event
func (e Event) MarshalJSON() ([]byte, error) {
    type Alias Event // Prevent infinite recursion
    
    return json.Marshal(&struct {
        CreatedAt string `json:"created_at"`
        UpdatedAt string `json:"updated_at"`
        *Alias
    }{
        CreatedAt: e.CreatedAt.Format(time.RFC3339),
        UpdatedAt: e.UpdatedAt.Format(time.RFC3339),
        Alias:     (*Alias)(&e),
    })
}

// Custom unmarshaling
func (e *Event) UnmarshalJSON(data []byte) error {
    type Alias Event
    
    aux := &struct {
        CreatedAt string `json:"created_at"`
        UpdatedAt string `json:"updated_at"`
        *Alias
    }{
        Alias: (*Alias)(e),
    }
    
    if err := json.Unmarshal(data, aux); err != nil {
        return err
    }
    
    var err error
    e.CreatedAt, err = time.Parse(time.RFC3339, aux.CreatedAt)
    if err != nil {
        return err
    }
    
    e.UpdatedAt, err = time.Parse(time.RFC3339, aux.UpdatedAt)
    return err
}

func main() {
    event := Event{
        ID:          1,
        Title:       "Go Workshop",
        Description: "Learn Go programming",
        CreatedAt:   time.Now(),
        UpdatedAt:   time.Now(),
    }
    
    jsonData, _ := json.MarshalIndent(event, "", "  ")
    fmt.Println("Custom JSON:")
    fmt.Println(string(jsonData))
}

Streaming JSON

For large JSON data, use streaming:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "strings"
)

func main() {
    jsonData := `[
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 25},
        {"name": "Charlie", "age": 35}
    ]`
    
    decoder := json.NewDecoder(strings.NewReader(jsonData))
    
    // Read opening bracket
    _, err := decoder.Token()
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    // Read array elements
    for decoder.More() {
        var person map[string]interface{}
        err := decoder.Decode(&person)
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
        
        fmt.Printf("Person: %+v\n", person)
    }
    
    // Read closing bracket
    _, err = decoder.Token()
    if err != nil {
        fmt.Println("Error:", err)
    }
}

JSON Lines (NDJSON)

Handle JSON Lines format:

package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "strings"
)

type LogEntry struct {
    Timestamp string `json:"timestamp"`
    Level     string `json:"level"`
    Message   string `json:"message"`
}

func main() {
    jsonLines := `{"timestamp":"2023-01-01T10:00:00Z","level":"INFO","message":"Application started"}
{"timestamp":"2023-01-01T10:01:00Z","level":"WARN","message":"High memory usage"}
{"timestamp":"2023-01-01T10:02:00Z","level":"ERROR","message":"Database connection failed"}`
    
    scanner := bufio.NewScanner(strings.NewReader(jsonLines))
    
    for scanner.Scan() {
        var entry LogEntry
        err := json.Unmarshal([]byte(scanner.Text()), &entry)
        if err != nil {
            fmt.Println("Error parsing line:", err)
            continue
        }
        
        fmt.Printf("[%s] %s: %s\n", entry.Level, entry.Timestamp, entry.Message)
    }
}

Error Handling

Robust error handling for JSON operations:

package main

import (
    "encoding/json"
    "fmt"
)

func safeJSONOperation(jsonStr string) {
    var data interface{}
    
    // Check for valid JSON first
    if !json.Valid([]byte(jsonStr)) {
        fmt.Println("Invalid JSON format")
        return
    }
    
    err := json.Unmarshal([]byte(jsonStr), &data)
    if err != nil {
        fmt.Println("Error parsing JSON:", err)
        return
    }
    
    // Type assertion with check
    if obj, ok := data.(map[string]interface{}); ok {
        if name, exists := obj["name"]; exists {
            if nameStr, ok := name.(string); ok {
                fmt.Println("Name:", nameStr)
            } else {
                fmt.Println("Name is not a string")
            }
        } else {
            fmt.Println("Name field not found")
        }
    } else {
        fmt.Println("JSON is not an object")
    }
}

func main() {
    safeJSONOperation(`{"name": "Test", "value": 123}`)
    safeJSONOperation(`invalid json`)
    safeJSONOperation(`{"name": 456}`) // Name is not a string
}

Best Practices

  1. Use struct tags: Control JSON field names and behavior
  2. Handle errors: Always check for JSON parsing errors
  3. Validate input: Use json.Valid() for untrusted input
  4. Use pointers for optional fields: Use *string for nullable fields
  5. Avoid interface{}: Use concrete types when possible
  6. Use MarshalIndent for debugging: Pretty-print JSON for development
  7. Consider streaming: For large JSON data
  8. Handle time fields properly: Use custom marshaling for timestamps
  9. Use consistent naming: Follow JSON naming conventions
  10. Test JSON operations: Include JSON tests in your test suite

Go’s JSON package is powerful and efficient. With proper struct definitions and error handling, you can work with JSON data reliably in your Go applications.

For more on file operations, check our file I/O tutorial. If you need to build HTTP clients, see the HTTP clients tutorial.

Last updated on