DevToolBoxGRATIS
Blog

JSON a struct Go: Guida completa alla conversione con esempi

9 min di letturadi DevToolBox

Working with JSON in Go requires defining struct types that mirror your JSON structure. Unlike dynamically typed languages where you can access JSON properties on the fly, Go demands explicit struct definitions with proper json tags for serialization and deserialization. This guide provides a thorough walkthrough of JSON to Go struct conversion, covering type mapping, nested structures, optional fields with pointer types, omitempty best practices, custom marshaling, and common real-world patterns you will encounter when building Go APIs and services.

Convert JSON to Go structs instantly with our free online tool.

JSON to Go Type Mapping Basics

Every JSON type has a corresponding Go type. Understanding these mappings is the foundation of writing correct Go struct definitions:

JSON Type          Go Type              Example
─────────────────────────────────────────────────────────
string             string               "hello" → "hello"
number (integer)   int / int64          42 → 42
number (float)     float64              3.14 → 3.14
boolean            bool                 true → true
null               *T (pointer)         null → nil
object             struct               {...} → MyStruct{}
array of strings   []string             ["a","b"] → []string{"a","b"}
array of objects   []MyStruct           [{...}] → []MyStruct{...}
array of mixed     []interface{}        [1,"a",true] → []interface{}{...}
dynamic object     map[string]any       {...} → map[string]any{...}

The most important distinction is how Go handles null values. A regular string or int field cannot be null in Go. To represent nullable fields, you must use pointer types (*string, *int). This is one of the most common mistakes when converting JSON to Go structs.

JSON Struct Tags: The Essential Guide

Struct tags tell Go's encoding/json package how to map between JSON keys and struct fields. The tag format is `json:"key_name,options"`:

package main

import (
    "encoding/json"
    "fmt"
)

// Basic struct with json tags
type User struct {
    // Field name → json key mapping
    ID        int    `json:"id"`              // "id" in JSON
    FirstName string `json:"first_name"`      // "first_name" in JSON
    LastName  string `json:"last_name"`       // "last_name" in JSON
    Email     string `json:"email"`           // "email" in JSON
    Age       int    `json:"age,omitempty"`   // skip if zero value
    Password  string `json:"-"`               // always skip (never serialize)

    // Without tag - uses field name as-is (case-insensitive unmarshal)
    Username string                            // matches "Username", "username", etc.
}

func main() {
    jsonData := `{
        "id": 1,
        "first_name": "Alice",
        "last_name": "Smith",
        "email": "alice@example.com",
        "age": 0,
        "Username": "alice123"
    }`

    var user User
    if err := json.Unmarshal([]byte(jsonData), &user); err != nil {
        panic(err)
    }

    fmt.Printf("Name: %s %s\n", user.FirstName, user.LastName)
    // Output: Name: Alice Smith

    // Marshal back - age is omitted because it's 0 (omitempty)
    output, _ := json.MarshalIndent(user, "", "  ")
    fmt.Println(string(output))
}

Key rules for struct tags: fields must be exported (start with uppercase letter) to be visible to the JSON package. The tag maps the exported Go field name to the JSON key name. Multiple options are comma-separated with no spaces.

Handling Nested JSON Objects

Real-world JSON payloads are rarely flat. Nested objects require separate struct definitions for each level:

// JSON input:
// {
//   "id": 1,
//   "name": "Acme Corp",
//   "address": {
//     "street": "123 Main St",
//     "city": "Springfield",
//     "state": "IL",
//     "zip": "62701",
//     "coordinates": {
//       "lat": 39.7817,
//       "lng": -89.6501
//     }
//   },
//   "contacts": [
//     { "name": "Alice", "role": "CEO", "email": "alice@acme.com" },
//     { "name": "Bob", "role": "CTO", "email": "bob@acme.com" }
//   ]
// }

// Define separate structs for each nested level
type Coordinates struct {
    Lat float64 `json:"lat"`
    Lng float64 `json:"lng"`
}

type Address struct {
    Street      string      `json:"street"`
    City        string      `json:"city"`
    State       string      `json:"state"`
    Zip         string      `json:"zip"`
    Coordinates Coordinates `json:"coordinates"`
}

type Contact struct {
    Name  string `json:"name"`
    Role  string `json:"role"`
    Email string `json:"email"`
}

type Company struct {
    ID       int       `json:"id"`
    Name     string    `json:"name"`
    Address  Address   `json:"address"`
    Contacts []Contact `json:"contacts"`
}

For deeply nested JSON, define each struct type separately rather than using inline anonymous structs. This improves readability, enables type reuse, and makes testing easier. Anonymous structs should only be used for one-off shapes that will never be reused.

Arrays and Slices

JSON arrays map to Go slices. The element type depends on the array contents:

// Different array types in JSON → Go
type Product struct {
    Name       string   `json:"name"`
    Tags       []string `json:"tags"`        // ["electronics", "sale"]
    Prices     []float64 `json:"prices"`     // [29.99, 39.99, 49.99]
    Ratings    []int    `json:"ratings"`      // [5, 4, 5, 3]
    IsActive   []bool   `json:"flags"`        // [true, false, true]

    // Array of objects
    Reviews    []Review `json:"reviews"`

    // Nested array of arrays (matrix)
    Matrix     [][]int  `json:"matrix"`       // [[1,2],[3,4]]

    // Mixed-type array (rare but possible)
    Metadata   []interface{} `json:"metadata"` // [1, "hello", true]
}

type Review struct {
    Author  string `json:"author"`
    Rating  int    `json:"rating"`
    Comment string `json:"comment"`
}

// Empty arrays vs null arrays
type Response struct {
    // null in JSON → nil slice (len=0, cap=0, == nil)
    // [] in JSON → empty slice (len=0, cap=0, != nil)
    // omitempty skips nil slices but NOT empty slices
    Items []Item `json:"items,omitempty"`
}

Handling Optional and Nullable Fields

One of the trickiest parts of JSON to Go conversion is handling optional fields correctly. Go has three patterns for this:

// Pattern 1: omitempty - skip zero values when marshaling
type UpdateRequest struct {
    Name  string `json:"name,omitempty"`  // "" is skipped
    Age   int    `json:"age,omitempty"`   // 0 is skipped
    Admin bool   `json:"admin,omitempty"` // false is skipped
}

// Problem: Can't distinguish "age not provided" from "age is 0"

// Pattern 2: Pointer types - distinguish null/missing from zero
type UpdateRequestV2 struct {
    Name  *string `json:"name,omitempty"`  // nil = not provided, "" = empty
    Age   *int    `json:"age,omitempty"`   // nil = not provided, 0 = zero
    Admin *bool   `json:"admin,omitempty"` // nil = not provided, false = false
}

// Helper function to create pointers (Go doesn't allow &literal)
func ptr[T any](v T) *T { return &v }

// Usage:
// req := UpdateRequestV2{
//     Name: ptr("Alice"),
//     Age:  ptr(0),      // explicitly set to 0, not nil
// }

// Pattern 3: json.RawMessage - defer parsing
type Event struct {
    Type    string          `json:"type"`
    Payload json.RawMessage `json:"payload"` // raw JSON bytes
}

// Parse payload based on type
func (e *Event) ParsePayload() (interface{}, error) {
    switch e.Type {
    case "user_created":
        var u User
        return &u, json.Unmarshal(e.Payload, &u)
    case "order_placed":
        var o Order
        return &o, json.Unmarshal(e.Payload, &o)
    default:
        var m map[string]interface{}
        return &m, json.Unmarshal(e.Payload, &m)
    }
}

Choose the right pattern based on your use case: use omitempty alone for fields where the zero value is acceptable, use pointer types when you need to distinguish between "not provided" and "zero value", and use json.RawMessage for polymorphic fields where the type depends on another field.

Custom JSON Marshaling and Unmarshaling

Sometimes the default JSON mapping is not sufficient. Go allows you to implement the json.Marshaler and json.Unmarshaler interfaces for custom serialization logic:

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

// Custom date format (JSON uses "2006-01-02", not RFC3339)
type Date struct {
    time.Time
}

const dateFormat = "2006-01-02"

func (d *Date) UnmarshalJSON(data []byte) error {
    // Remove quotes from JSON string
    s := strings.Trim(string(data), `"`)
    if s == "null" || s == "" {
        return nil
    }
    t, err := time.Parse(dateFormat, s)
    if err != nil {
        return fmt.Errorf("invalid date format: %s", s)
    }
    d.Time = t
    return nil
}

func (d Date) MarshalJSON() ([]byte, error) {
    if d.Time.IsZero() {
        return []byte("null"), nil
    }
    return json.Marshal(d.Time.Format(dateFormat))
}

// Usage in a struct
type Employee struct {
    Name      string `json:"name"`
    StartDate Date   `json:"start_date"`
    EndDate   *Date  `json:"end_date,omitempty"`
}

// Custom enum type
type Status string

const (
    StatusActive   Status = "active"
    StatusInactive Status = "inactive"
    StatusPending  Status = "pending"
)

func (s *Status) UnmarshalJSON(data []byte) error {
    var str string
    if err := json.Unmarshal(data, &str); err != nil {
        return err
    }
    switch Status(str) {
    case StatusActive, StatusInactive, StatusPending:
        *s = Status(str)
        return nil
    default:
        return fmt.Errorf("invalid status: %s", str)
    }
}

Common use cases for custom marshaling include: handling multiple date formats, converting between string and numeric representations, implementing enum types, and flattening nested structures during serialization.

Real-World API Patterns

Here are common JSON patterns you will encounter when working with REST APIs and how to model them in Go:

// Pattern: Paginated API Response
type PaginatedResponse[T any] struct {
    Data       []T    `json:"data"`
    Total      int    `json:"total"`
    Page       int    `json:"page"`
    PerPage    int    `json:"per_page"`
    TotalPages int    `json:"total_pages"`
    NextURL    string `json:"next_url,omitempty"`
    PrevURL    string `json:"prev_url,omitempty"`
}

// Pattern: API Error Response
type APIError struct {
    Code    int               `json:"code"`
    Message string            `json:"message"`
    Details map[string]string `json:"details,omitempty"`
}

func (e *APIError) Error() string {
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// Pattern: Wrapper for success/error responses
type APIResponse[T any] struct {
    Success bool      `json:"success"`
    Data    *T        `json:"data,omitempty"`
    Error   *APIError `json:"error,omitempty"`
}

// Pattern: Webhook payload with dynamic event data
type WebhookPayload struct {
    ID        string          `json:"id"`
    Event     string          `json:"event"`
    Timestamp time.Time       `json:"timestamp"`
    Data      json.RawMessage `json:"data"`
}

// Pattern: Configuration file with defaults
type Config struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    Debug    bool   `json:"debug"`
    LogLevel string `json:"log_level"`
    Database struct {
        URL             string `json:"url"`
        MaxConnections  int    `json:"max_connections"`
        ConnectTimeout  int    `json:"connect_timeout_ms"`
    } `json:"database"`
}

// Set defaults before unmarshaling
func NewConfig() *Config {
    return &Config{
        Host:     "localhost",
        Port:     8080,
        LogLevel: "info",
    }
}

func LoadConfig(data []byte) (*Config, error) {
    cfg := NewConfig()
    if err := json.Unmarshal(data, cfg); err != nil {
        return nil, err
    }
    return cfg, nil
}

Automating JSON to Go Struct Generation

Manually writing struct definitions for large JSON payloads is tedious and error-prone. Our JSON to Go converter tool automatically generates properly tagged, correctly typed struct definitions from any JSON input. It handles nested objects, arrays, optional fields, and mixed types automatically.

Convert JSON to Go structs instantly with our free online tool.

For CI/CD integration, you can also use the json-to-go command-line tool or generate Go types from JSON Schema definitions using go-jsonschema.

# Generate Go structs from a JSON file
cat api-response.json | json-to-go

# Generate from a JSON Schema definition
go install github.com/atombender/go-jsonschema/cmd/gojsonschema@latest
gojsonschema -p models schema.json

# Validate your structs handle the JSON correctly
go test -run TestUnmarshal ./models/...

Frequently Asked Questions

How do I convert JSON to a Go struct?

Define a Go struct with exported fields and json tags that match your JSON keys. Map JSON types to Go types (string to string, number to int/float64, boolean to bool, null to pointer types, object to struct, array to slice). Use our free online JSON to Go converter tool for automatic generation.

What is omitempty in Go JSON tags?

The omitempty option in a json struct tag tells the JSON encoder to skip the field when marshaling if it has a zero value (empty string, 0, false, nil pointer, empty slice/map). This is useful for PATCH API requests where you only want to send changed fields.

How do I handle null values in Go JSON?

Use pointer types (*string, *int, *bool) for fields that can be null in JSON. When the JSON value is null, the pointer will be nil in Go. Regular (non-pointer) types cannot represent null and will use their zero value instead.

Can I have different JSON field names than Go field names?

Yes, use struct tags. For example, `json:"user_name"` maps the JSON key "user_name" to whatever Go field name you choose (e.g., UserName). The json tag completely controls the JSON serialization name.

How do I handle dynamic or unknown JSON fields in Go?

Use map[string]interface{} (or map[string]any in Go 1.18+) for completely dynamic JSON, json.RawMessage for fields you want to delay parsing, or embed a map alongside known fields. For partially known structures, combine a struct with a map.

Converting JSON to Go structs correctly is fundamental to writing robust Go applications. Understanding type mappings, pointer types for nullable fields, omitempty behavior, and custom marshaling patterns will make your JSON handling reliable and maintainable. For quick conversions, use our free online tool. For production codebases, establish consistent struct tag conventions and consider generating types from JSON Schema for API contracts.

Try the JSON to Go struct converter now.

Related Developer Tools and Guides

𝕏 Twitterin LinkedIn
È stato utile?

Resta aggiornato

Ricevi consigli dev e nuovi strumenti ogni settimana.

Niente spam. Cancella quando vuoi.

Prova questi strumenti correlati

GoJSON to Go Struct{ }JSON Formatter

Articoli correlati

JSON in Go Struct: Strategie di mapping e best practice

Padroneggia la conversione da JSON a struct Go. Tag struct, tipi annidati, omitempty, marshaling personalizzato e pattern reali.

JSON vs YAML vs TOML: Quale formato di configurazione usare?

Confronta i formati di configurazione JSON, YAML e TOML.

REST API Best Practices: La guida completa per il 2026

Impara le best practice di progettazione REST API: convenzioni di denominazione, gestione errori, autenticazione e sicurezza.