DevToolBoxGRATIS
Blog

JSON in Struct Go: La Guida Completa alla Conversione per il 2026

11 min di letturadi DevToolBox
TL;DR

Convert JSON to Go structs using encoding/json struct tags, pointer types for nullable fields, and json.Decoder for HTTP streaming. Define exported fields with `json:"name"` tags, use *T for optional values, and implement UnmarshalJSON for custom types like dates. For high-throughput services, replace encoding/json with jsoniter or sonic with zero code changes.

1. Why Use Go Structs for JSON?

Go is statically typed. When you reach for map[string]interface{} to handle JSON you trade compile-time safety for tedious type assertions and runtime panics. Proper struct definitions give you IDE autocompletion, exhaustive refactoring, and zero-overhead marshaling via reflection cache.

ApproachType SafetyIDE SupportPerformanceMaintainability
Go StructCompile-timeFull autocompleteFast (cached reflection)High โ€” refactor-safe
map[string]interface{}None โ€” panics at runtimeNo completionModerateLow โ€” string keys
json.RawMessageDeferred onlyPartialFastest (skip parse)Medium โ€” delayed decode

2. Basic json.Unmarshal and json.Marshal

The encoding/json package is in the Go standard library โ€” no dependencies needed. Struct fields must be exported (uppercase) and annotated with a `json:"..."` tag.

Defining the struct and unmarshaling

package main

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

// Exported fields + json tags are required for encoding/json to see them.
type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Active    bool      `json:"active"`
    Score     float64   `json:"score"`
    CreatedAt time.Time `json:"created_at"`
}

func main() {
    data := []byte(`{
        "id": 42,
        "name": "Alice",
        "email": "alice@example.com",
        "active": true,
        "score": 98.6,
        "created_at": "2026-01-15T09:00:00Z"
    }`)

    var user User
    if err := json.Unmarshal(data, &user); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("User: %s (%.1f)\n", user.Name, user.Score)
    // User: Alice (98.6)
}

Encoding (Marshal) back to JSON

// Compact JSON
out, err := json.Marshal(user)
// {"id":42,"name":"Alice","email":"alice@example.com","active":true,"score":98.6,"created_at":"2026-01-15T09:00:00Z"}

// Pretty-printed JSON
out, err = json.MarshalIndent(user, "", "  ")
// {
//   "id": 42,
//   "name": "Alice",
//   ...
// }

// Write JSON directly to an http.ResponseWriter (no intermediate buffer)
func writeJSON(w http.ResponseWriter, v any) error {
    w.Header().Set("Content-Type", "application/json")
    return json.NewEncoder(w).Encode(v)
}

Error handling best practice

func parseUser(data []byte) (*User, error) {
    var u User
    if err := json.Unmarshal(data, &u); err != nil {
        return nil, fmt.Errorf("parseUser: %w", err)
    }
    if u.Name == "" {
        return nil, fmt.Errorf("parseUser: name is required")
    }
    return &u, nil
}

3. JSON Struct Tags

Tags are raw string literals of the form `json:"key,options"`. They sit after the field type, surrounded by backticks.

type Product struct {
    // Map Go CamelCase to JSON snake_case
    ProductID   int     `json:"product_id"`

    // omitempty: skip field in output when zero-value ("", 0, false, nil)
    Description string  `json:"description,omitempty"`

    // "-": always exclude this field from JSON (passwords, internals)
    InternalRef string  `json:"-"`

    // "string": encode number as a quoted JSON string
    // Useful for JavaScript Number precision (int64 > 2^53)
    BigID       int64   `json:"big_id,string"`

    // No tag: uses the exact Go field name ("Type")
    Type        string
}

// Combined options: snake_case key + omitempty
type Event struct {
    StartedAt *time.Time `json:"started_at,omitempty"` // null or absent โ†’ omit
    EndedAt   *time.Time `json:"ended_at,omitempty"`
}
Tag OptionEffect on MarshalEffect on Unmarshal
`json:"key"`Output key is keyReads from JSON field key
`,omitempty`Skip if zero-valueNo effect
`json:"-"`Always excludedAlways ignored
`,string`Number as quoted stringExpects quoted string

4. Nested Structs, Arrays, and Maps

Nest structs for JSON objects, use slices for JSON arrays, and maps for dictionary-like objects with dynamic keys.

// JSON:
// {
//   "id": 1,
//   "customer": { "name": "Alice", "address": { "city": "NYC", "zip": "10001" } },
//   "items": [{ "sku": "ABC", "qty": 2, "price": 9.99 }],
//   "tags": ["urgent", "vip"],
//   "meta": { "source": "web", "campaign": "summer" }
// }

type Order struct {
    ID       int               `json:"id"`
    Customer Customer          `json:"customer"`       // nested struct
    Items    []LineItem        `json:"items"`          // slice of structs
    Tags     []string          `json:"tags"`           // slice of primitives
    Meta     map[string]string `json:"meta,omitempty"` // map with string values
}

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

type Address struct {
    City   string `json:"city"`
    Street string `json:"street,omitempty"`
    Zip    string `json:"zip,omitempty"`
}

type LineItem struct {
    SKU      string  `json:"sku"`
    Quantity int     `json:"qty"`
    Price    float64 `json:"price"`
}

// Decode the whole nested tree at once:
var order Order
json.Unmarshal(data, &order)
fmt.Println(order.Customer.Address.City) // NYC
fmt.Println(order.Items[0].SKU)          // ABC

Pointer fields for optional nested objects

// If "shipping" may be absent from JSON:
type Order struct {
    ID       int       `json:"id"`
    Shipping *Address  `json:"shipping,omitempty"` // nil if absent/null
}

// Check before access:
if order.Shipping != nil {
    fmt.Println(order.Shipping.City)
}

5. Optional Fields with Pointer Types

In Go there is no concept of "undefined". A string field missing from JSON gets the zero value "" โ€” indistinguishable from an empty string that was explicitly set. Use *string, *int, etc. to distinguish absent from zero.

// JSON variants:
// { "id": 1, "name": "Alice", "bio": null }  โ†’ bio is explicitly null
// { "id": 2, "name": "Bob" }                 โ†’ bio is absent
// { "id": 3, "name": "Carol", "bio": "Dev" } โ†’ bio has a value

type UserProfile struct {
    ID   int     `json:"id"`
    Name string  `json:"name"`
    Bio  *string `json:"bio"`              // nil for both null and absent
    Age  *int    `json:"age,omitempty"`    // omit entirely when nil
}

// Safe access pattern:
func displayBio(u UserProfile) string {
    if u.Bio != nil {
        return *u.Bio
    }
    return "(no bio)"
}

// Setting a pointer field:
bio := "Senior Gopher"
u := UserProfile{ID: 1, Name: "Alice", Bio: &bio}

// Helper to get a pointer to any value (Go 1.18+ generic):
func ptr[T any](v T) *T { return &v }

u2 := UserProfile{ID: 2, Name: "Bob", Bio: ptr("Backend Dev"), Age: ptr(30)}

6. Custom UnmarshalJSON โ€” Parsing Dates and Mixed Types

Implement the json.Unmarshaler interface (UnmarshalJSON([]byte) error) to control how a type is decoded. The reciprocal json.Marshaler interface controls encoding.

Custom date format (YYYY-MM-DD)

// time.Time uses RFC3339 by default ("2006-01-02T15:04:05Z07:00")
// To parse plain dates like "2006-01-02":

type Date struct{ time.Time }

func (d *Date) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    t, err := time.Parse("2006-01-02", s)
    if err != nil {
        return fmt.Errorf("Date.UnmarshalJSON: %w", err)
    }
    d.Time = t
    return nil
}

func (d Date) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.Format("2006-01-02"))
}

type Subscription struct {
    UserID    int  `json:"user_id"`
    StartDate Date `json:"start_date"` // parses "2026-01-15"
    EndDate   Date `json:"end_date"`
}

Mixed-type / discriminated union fields

// JSON: { "type": "user", "data": { "name": "Alice" } }
//   or: { "type": "product", "data": { "sku": "ABC" } }

type Envelope struct {
    Type string          `json:"type"`
    Data json.RawMessage `json:"data"` // delay decode until type is known
}

func (e *Envelope) Decode() (any, error) {
    switch e.Type {
    case "user":
        var u User
        return &u, json.Unmarshal(e.Data, &u)
    case "product":
        var p Product
        return &p, json.Unmarshal(e.Data, &p)
    default:
        return nil, fmt.Errorf("unknown type: %s", e.Type)
    }
}

7. json.Decoder for Streaming Large JSON

json.NewDecoder wraps any io.Reader and streams data without loading everything into memory. This is the idiomatic approach for HTTP handlers.

Decode a single object from HTTP body

func createUser(w http.ResponseWriter, r *http.Request) {
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields() // reject unexpected keys

    var u User
    if err := dec.Decode(&u); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // u is now populated
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(u)
}

Stream a large JSON array line by line

// Process millions of JSON objects from a file without loading all into memory:
func streamUsers(r io.Reader) error {
    dec := json.NewDecoder(r)

    // Read opening '['
    if _, err := dec.Token(); err != nil {
        return err
    }

    for dec.More() {
        var u User
        if err := dec.Decode(&u); err != nil {
            return err
        }
        process(u) // handle each user without keeping all in memory
    }

    // Read closing ']'
    _, err := dec.Token()
    return err
}

DisallowUnknownFields for strict validation

// By default, extra JSON keys are silently ignored.
// Use DisallowUnknownFields to error on unexpected keys:

dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()

var payload CreateUserRequest
if err := dec.Decode(&payload); err != nil {
    // err will mention "unknown field \"extraKey\"" if present
    http.Error(w, "invalid request: "+err.Error(), http.StatusBadRequest)
    return
}

8. Struct Tags with go-playground/validator

Combine json tags with validate tags from the popular github.com/go-playground/validator/v10 library to add input validation without separate validation logic.

// go get github.com/go-playground/validator/v10

import "github.com/go-playground/validator/v10"

type CreateUserRequest struct {
    Name     string `json:"name"     validate:"required,min=2,max=100"`
    Email    string `json:"email"    validate:"required,email"`
    Age      int    `json:"age"      validate:"gte=0,lte=130"`
    Role     string `json:"role"     validate:"oneof=admin user viewer"`
    Password string `json:"password" validate:"required,min=8"`
}

var validate = validator.New()

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "bad JSON: "+err.Error(), http.StatusBadRequest)
        return
    }

    if err := validate.Struct(req); err != nil {
        // Returns structured validation errors
        errs := err.(validator.ValidationErrors)
        http.Error(w, errs.Error(), http.StatusUnprocessableEntity)
        return
    }

    // req is valid, proceed
}

Common validator tags

Name     string `validate:"required,min=2,max=100"`     // non-empty, length 2-100
Email    string `validate:"required,email"`              // RFC 5322 email format
Age      int    `validate:"gte=0,lte=130"`               // 0 โ‰ค age โ‰ค 130
URL      string `validate:"url"`                         // valid URL
UUID     string `validate:"uuid4"`                       // UUID v4 format
OneOf    string `validate:"oneof=red green blue"`        // enum check
Nested   *Inner `validate:"required"`                    // non-nil pointer

9. Go 1.21+ and the Upcoming encoding/json/v2

The Go team is developing encoding/json/v2 (tracked in golang.org/x/exp/jsonv2) with several long-requested improvements. It is not yet stable but worth knowing about.

// Key improvements in encoding/json/v2 (experimental as of 2026):
//
// 1. omitzero: omit fields when they are the zero value of their type
//    (different from omitempty which uses interface-based zero check)
type Config struct {
    Timeout int `json:",omitzero"` // omit when Timeout == 0
}
//
// 2. Better error messages with field path info:
//    json: cannot unmarshal string into Go struct field User.age of type int
//    (previously just "cannot unmarshal")
//
// 3. Strict mode: unknown fields are errors by default
//    (flip of current behavior)
//
// 4. Deterministic map ordering (sorted keys)
//
// 5. Support for encoding.TextMarshaler on map keys
//
// Migration: the v2 API is largely compatible with v1.
// Change the import path and fix any breaking changes.

10. Popular JSON Libraries Comparison

encoding/json is correct and battle-tested. Switch to a faster library only when JSON parsing is a proven bottleneck in your profiler.

LibrarySpeed vs stdKey FeatureBest Use Case
encoding/jsonBaseline (1x)Zero dependencies, stdlibDefault for all Go projects
easyjson~3โ€“5x fasterCode generation (no reflection)High-throughput APIs, fixed schemas
jsoniter~2โ€“3x fasterDrop-in replacement (import jsoniter "github.com/json-iterator/go")Performance upgrade with no refactoring
sonic~6โ€“10x fasterSIMD JIT (amd64 only)Ultra-high throughput, Bytedance-scale
// Drop-in replacement with jsoniter (same API as encoding/json):
import jsoniter "github.com/json-iterator/go"

var json = jsoniter.ConfigCompatibleWithStandardLibrary

// Now use json.Marshal / json.Unmarshal / json.NewDecoder as before.
// No other code changes needed.

11. HTTP API with JSON โ€” Full Pattern

The idiomatic Go pattern for JSON APIs uses json.NewDecoder for reading request bodies and json.NewEncoder for writing responses โ€” both streaming without intermediate byte slices.

GET โ€” fetch and decode JSON

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type GitHubUser struct {
    Login     string    `json:"login"`
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    CreatedAt time.Time `json:"created_at"`
}

func fetchGitHubUser(username string) (*GitHubUser, error) {
    url := "https://api.github.com/users/" + username
    resp, err := http.Get(url)
    if err != nil {
        return nil, fmt.Errorf("GET %s: %w", url, err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
    }

    var user GitHubUser
    if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
        return nil, fmt.Errorf("decode response: %w", err)
    }
    return &user, nil
}

POST โ€” encode and send JSON

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

type CreateUserReq struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type CreateUserResp struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func createUser(baseURL string, req CreateUserReq) (*CreateUserResp, error) {
    var buf bytes.Buffer
    if err := json.NewEncoder(&buf).Encode(req); err != nil {
        return nil, err
    }

    resp, err := http.Post(baseURL+"/users", "application/json", &buf)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var result CreateUserResp
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, fmt.Errorf("decode response: %w", err)
    }
    return &result, nil
}

Full HTTP handler with validation

// GET /users/:id โ€” respond with JSON
func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // Go 1.22+

    user, err := db.FindUser(id)
    if err != nil {
        writeError(w, http.StatusNotFound, "user not found")
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

// POST /users โ€” accept JSON body
func postUser(w http.ResponseWriter, r *http.Request) {
    if r.Header.Get("Content-Type") != "application/json" {
        writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be application/json")
        return
    }

    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()

    var req CreateUserReq
    if err := dec.Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    user, err := db.CreateUser(req)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "could not create user")
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func writeError(w http.ResponseWriter, code int, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(map[string]string{"error": msg})
}

12. Common Pitfalls

1. Unexported fields are silently ignored

// โŒ Wrong โ€” fields start with lowercase, invisible to encoding/json
type user struct {
    id   int
    name string
}
data, _ := json.Marshal(user{id: 1, name: "Alice"})
// Output: {}  โ† empty!

// โœ… Correct โ€” exported fields with json tags
type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}
data, _ = json.Marshal(User{ID: 1, Name: "Alice"})
// Output: {"id":1,"name":"Alice"}

2. JSON numbers become float64 in interface{}

var m map[string]interface{}
json.Unmarshal([]byte(`{"count": 42}`), &m)

// โŒ Panics โ€” it's float64, not int!
count := m["count"].(int)

// โœ… Correct type assertion
count := int(m["count"].(float64))

// โœ… Better: use json.Number to preserve original representation
dec := json.NewDecoder(strings.NewReader(`{"count": 42}`))
dec.UseNumber()
dec.Decode(&m)
n, _ := m["count"].(json.Number).Int64() // 42 exactly

3. Date format gotchas

// โŒ time.Time only parses RFC3339 by default
// JSON: { "date": "2026-01-15" }  โ† no time component
var s struct{ Date time.Time `json:"date"` }
json.Unmarshal(data, &s) // ERROR: cannot parse "2026-01-15" as RFC3339

// โœ… Use a custom Date type (see Section 6)
// OR use a string and parse manually:
var s2 struct{ Date string `json:"date"` }
json.Unmarshal(data, &s2)
t, _ := time.Parse("2006-01-02", s2.Date)

4. Nil slice vs empty array

// nil slice marshals as JSON null
var items []string         // nil
json.Marshal(items)        // null

// Initialized empty slice marshals as []
items = make([]string, 0)
json.Marshal(items)        // []

// This matters when your frontend expects an array, not null:
type Response struct {
    Items []string `json:"items"`
}
// Always initialize: Items: make([]string, 0)

5. Circular references cause infinite loop

// โŒ Circular reference โ€” json.Marshal will panic with stack overflow
type Node struct {
    Value    int
    Children []*Node `json:"children"`
    Parent   *Node   `json:"parent"` // points back โ†’ cycle!
}

// โœ… Exclude back-references with json:"-"
type Node struct {
    Value    int
    Children []*Node `json:"children"`
    Parent   *Node   `json:"-"`        // excluded from JSON
}

Frequently Asked Questions

How do I convert JSON to Go struct online?

Use DevToolBox's JSON to Go converter โ€” paste your JSON and get idiomatic Go struct definitions with json tags, pointer types for nullable fields, and proper Go naming conventions instantly. It handles nested objects, arrays, and mixed types automatically.

Should I use json.Unmarshal or json.NewDecoder?

Use json.NewDecoder(r.Body).Decode(&v) for HTTP request/response bodies โ€” it streams without buffering all bytes. Use json.Unmarshal(data, &v) when you already have a []byte in memory (e.g. from a database or file).

How do I handle nullable JSON fields in Go?

Use pointer types: *string, *int, *bool. A nil pointer marshals to JSON null. Always check for nil before dereferencing: if u.Bio != nil { use *u.Bio }.

What does omitempty do in a Go json tag?

omitempty omits the field from JSON output when it has its zero value โ€” "" for strings, 0 for numbers, false for booleans, nil for pointers/slices/maps. Combine with a pointer: *string `json:"bio,omitempty"` to also omit null values.

Why are struct fields missing from JSON output?

Fields must start with an uppercase letter to be exported. Lowercase (unexported) fields are silently ignored by encoding/json. Also verify you have not accidentally used the `json:"-"` tag which always excludes a field.

How do I parse custom date formats in Go?

Create a custom type that embeds time.Time and implement UnmarshalJSONusing time.Parse("2006-01-02", s). Go's reference time is Mon Jan 2 15:04:05 MST 2006 โ€” use those exact values in your format string.

How do I reject unknown JSON fields?

Use dec.DisallowUnknownFields() on a json.Decoder. By default, encoding/json silently ignores extra keys. Strict mode is useful for API handlers where unexpected fields may indicate a client bug or schema mismatch.

Which JSON library is fastest in Go?

For amd64 workloads: sonic (ByteDance) is ~6-10x faster using SIMD JIT. jsoniter is a safe 2-3x improvement with zero code changes โ€” just replace the import. encoding/json remains the best default unless JSON is a proven bottleneck.

Key Takeaways
  • Go struct fields must be exported (uppercase) to be visible to encoding/json
  • Use `json:"field_name"` tags to map Go names to JSON keys
  • Use `,omitempty` to skip zero-value fields in JSON output
  • Use `json:"-"` to permanently exclude sensitive fields
  • Pointer types (*string, *int) model nullable / optional JSON fields
  • Use json.NewDecoder(r.Body) for HTTP streams; json.Unmarshal for []byte
  • Call dec.DisallowUnknownFields() for strict API validation
  • Implement UnmarshalJSON / MarshalJSON for custom date formats and enums
  • JSON numbers decode as float64 into interface{} โ€” use typed structs or dec.UseNumber()
  • Initialize slices with make([]T, 0) when the API must return [] not null
  • For high-throughput APIs, replace encoding/json with jsoniter or sonic
๐• 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 FormatterTSJSON to TypeScriptโœ“JSON Validator

Articoli correlati

JSON in TypeScript Online: La guida completa per sviluppatori

Scopri come generare automaticamente tipi TypeScript da JSON. Interface vs type, campi opzionali/nullable, oggetti annidati, tipi union, validazione Zod, tipi risposta API generici e best practice tsconfig.

JSON in Dart: Guida Completa alla Generazione di Classi Flutter con Null Safety

Scopri come convertire JSON in classi Dart per app Flutter con null safety.