Decode JSON into Go structs safely with field tags, unknown-field checks, body limits, trailing-data validation, and useful error handling.
Last updated on

JSON to Struct in Go: Decoding, Validation, and Common Mistakes


Decoding JSON into a Go struct is usually one call to json.Unmarshal or Decoder.Decode. Production code needs a little more care: malformed input, unknown fields, oversized bodies, missing values, and multiple JSON documents all need deliberate handling.

Define the JSON shape with struct tags

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

Exported Go fields are eligible for JSON encoding. The tags define the external field names without forcing your Go identifiers to match them.

For JSON already stored in memory, use json.Unmarshal:

data := []byte(`{"name":"Ada","age":36,"email":"ada@example.com"}`)

var request CreateUserRequest
if err := json.Unmarshal(data, &request); err != nil {
    return fmt.Errorf("decode user: %w", err)
}

The decoder validates JSON syntax and type compatibility. It does not decide whether an empty name or zero age is valid for your application. Keep domain validation as a separate step.

Decode HTTP request bodies strictly

An HTTP endpoint should normally reject unknown fields. Otherwise a typo such as "emali" is silently ignored.

func decodeCreateUser(w http.ResponseWriter, r *http.Request) (CreateUserRequest, error) {
    r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB

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

    var request CreateUserRequest
    if err := decoder.Decode(&request); err != nil {
        return CreateUserRequest{}, fmt.Errorf("decode request body: %w", err)
    }

    // A valid request contains exactly one JSON value.
    if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
        return CreateUserRequest{}, errors.New("request body must contain one JSON object")
    }

    return request, nil
}

http.MaxBytesReader prevents an unbounded body from consuming excessive memory. DisallowUnknownFields catches client mistakes, and the second decode rejects trailing JSON such as {...}{...}.

For a complete handler and server setup, see building a REST API in Go.

Understand missing, zero, and null values

These inputs all produce different questions:

{}
{"age": 0}
{"age": null}

With an int field, all three leave Age as 0. If the distinction matters, use a pointer:

type UpdateUserRequest struct {
    Name *string `json:"name"`
    Age  *int    `json:"age"`
}

Now nil means the field was missing or null, while a non-nil pointer can contain the zero value. If missing and explicit null must also be distinguished, define a small custom type with UnmarshalJSON.

Model inconsistent external JSON carefully

Third-party APIs sometimes return a number as either JSON number or string. Do not immediately make every field any; that pushes type checks throughout the rest of the program.

Create a focused type instead:

type StringInt int

func (n *StringInt) UnmarshalJSON(data []byte) error {
    value := strings.Trim(string(data), `"`)
    parsed, err := strconv.Atoi(value)
    if err != nil {
        return fmt.Errorf("parse integer %q: %w", value, err)
    }
    *n = StringInt(parsed)
    return nil
}

This keeps the workaround at the boundary. The rest of the application still works with a concrete numeric type.

Generate structs, then review them

Tools such as JSON-to-Go and GoLand’s “Paste JSON as Struct” can create a useful first draft. Generated structs only reflect the sample you provide. Review optional fields, arrays that may be empty, numbers that may exceed int, time formats, and fields that can be null.

Do not treat one sample response as a complete schema.

Return useful decoding errors

Clients need a clear 400 response, while server logs need enough detail to diagnose the input problem. Keep the underlying error for logging, but avoid echoing an entire request body or internal implementation detail.

Use errors.As to recognize errors such as *json.SyntaxError or *json.UnmarshalTypeError when you want more specific messages. Go error wrapping and inspection covers the underlying pattern.

The safe default is: define a concrete boundary type, limit the input, reject fields you do not understand, decode exactly one value, and validate business rules separately. The encoding/json documentation contains the complete field-matching and tag rules.