Skip to content
Code2026-08-286 min read

How to Convert JSON to Go struct (with json tags and nested types)

Integrating a third-party API returns a big chunk of JSON. You don't want to hand-write a Go struct with dozens of fields—slow and error-prone with tags. Can you auto-convert?

Yes. Here are the type-mapping rules for JSON → Go struct, and how to generate code with json tags in one click.

JSON type to Go type mapping

JSON typeGo typeNotes
Object {}struct {…}Nested struct
Array []slice []TElement type inferred from content
String "a"string
Number (no decimal)int64 / float64Depends on decimal point
Number (decimal)float64
Booleanbool
nullpointer or interface{}Use *T to tell "zero" from "unset"
Mixed array[]interface{}Avoid if possible

Key pitfall: numbers and null

Go has no "nullable primitive". If a field may be null, writing int will error on decode—use a pointer:

type User struct {
    Name  string   `json:"name"`
    Age   *int     `json:"age"`    // may be null
    Score *float64 `json:"score"`
}

Step-by-step generation (no plugin)

Use this site's JSON to Go tool—runs locally, JSON never uploaded:

  1. Open JSON to Go.
  2. Paste your JSON (object or array).
  3. Configure:
    • JSON Tag: default Keep original keys so json:"…" matches the sample wire shape; choose snake_case / camelCase only when you intentionally rewrite tags; No json tag omits tags.
    • Export Style: PascalCase or camelCase for Go field identifiers (exported names are still capitalized for encoding/json).
    • Root type name (e.g. Response, User) and optional omitempty.
  4. Output updates as you type—copy the struct code into your project.
  5. Need other languages? Use JSON to TypeScript or JSON to Java.

Full example

Input JSON:

{
  "id": 1024,
  "name": "Alice",
  "isActive": true,
  "tags": ["go", "api"],
  "profile": {
    "age": 30,
    "city": "Shanghai"
  }
}

Generated Go:

type Profile struct {
    Age  int    `json:"age"`
    City string `json:"city"`
}

type Response struct {
    ID       int      `json:"id"`
    Name     string   `json:"name"`
    IsActive bool     `json:"isActive"`
    Tags     []string `json:"tags"`
    Profile  Profile  `json:"profile"`
}

Advanced tips

  • Format first: tidy messy JSON with the JSON formatter for more accurate inference.
  • Distinguish zero vs missing: use pointers for external API fields, so "not returned" isn't mistaken for "0 / empty".
  • Time fields: JSON times are usually RFC3339 strings; use time.Time and ensure tag/format align.
  • Exported fields: Go fields must be capitalized to be serialized by encoding/json—the tool handles this; don't miss it when writing by hand.

The three Go-specific gotchas: time, null, and interfaces

Go's JSON mapping has sharp edges that generic converters miss. Time: time.Time marshals to RFC 3339 strings, but unmarshaling fails on any other format — if your API emits epoch milliseconds (JS default), the generated struct needs a custom type or int64 plus conversion. null: null into a string field silently becomes "" (no error, data corruption by omission); into *string it becomes a proper nil. Every nullable API field should generate a pointer — check your converter emits *T for fields your API actually nulls. interface: heterogeneous arrays (a field that is sometimes an object, sometimes a string) collapse to interface{} and push type-switches into every consumer; a cleaner pattern is two explicit fields with json.RawMessage and decode-on-demand.

JSON tags must match the wire (not your Go taste)

The footgun: paste camelCase JSON (userName) while the tag dropdown is set to snake_case. Older defaults rewrote the tag to user_name, and json.Unmarshal then left the field zero forever—no compile error, just silent data loss. ToolVault’s default is now Keep original keys: the tag string equals the JSON key in the sample. Use snake/camel rewrite only when you are emitting a different convention on purpose (for example generating request bodies for an API that insists on snake keys while your Go fields stay idiomatic).

Large integers in the sample stay on the lossless path (int64 when the literal has no decimal); still verify IDs that exceed JS safe integers after you paste through other editors.

Struct tags worth adding by hand

The generated tags give you the minimum; three additions pay for themselves. omitempty on optional fields — otherwise your responses echo zero-values ("count":0) that consumers cannot distinguish from absent. ,string on numeric fields emitted as strings by legacy APIs (JS big-number paranoia). And field names: Go conventions want UserID while JSON says userId — let the tag carry the mismatch, not a non-idiomatic Go name. For versioned APIs, generate one struct per version rather than tolerating interface{} soup.

Round-trip testing the generated types

Structs that compile are not structs that round-trip: feed the original JSON through json.Unmarshal then json.Marshal and byte-compare (after normalizing key order). Differences surface exactly where the schema and Go disagree — null handling, number precision (float64 silently rounds large int64s; use json.Number for IDs), and dropped unknown fields. This ten-line test belongs in CI for every schema-bearing service.

FAQ

Q: Array elements have inconsistent types? Use []interface{} and assert yourself. Prefer a uniform element shape from the API when you can change it.

Q: Number sometimes int, sometimes decimal? float64 is safest; if definitely integer, change to int64 manually.

Q: Too deeply nested, too many structs? Use map[string]interface{} to receive temporarily, then define structs as needed, or convert in layers.

Q: Generate omitempty tags? Depends. For request bodies, omitempty drops zero-value fields; for responses, usually skip it to avoid treating "missing" as "zero".

Summary

JSON to Go struct = generate tagged structs by type mapping; use pointers for null, pick number types carefully. Use this site's JSON to Go tool to generate locally—JSON not uploaded, safer for sensitive payloads.

From sample JSON to stable API types

Treat the generator output as a draft: rename fields to domain language, collapse accidental nested structs, and add json tags that match the wire (including case). Keep a golden sample beside the struct and unmarshal it in a table test whenever the producer changes. When the sample uses integers that may exceed float64 precision, switch those fields to json.Number or int64 before the bug reaches production IDs. Pair with the JSON formatter to normalize fixtures, and the JSON to TypeScript tool when the same payload ships to a TS client.

Implementation notes

A pitfall fixed in the site's JSON-to-TypeScript tool generalizes to all type generation: heterogeneous arrays once silently dropped everything after the first element; the fix was to emit a union element type, named after the singularized key (users → User). Go has no union to fall back on, so when building a struct for a heterogeneous array, put several entries in the sample — don't let the first one speak for the whole batch.