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 type | Go type | Notes |
|---|---|---|
Object {} | struct {…} | Nested struct |
Array [] | slice []T | Element type inferred from content |
String "a" | string | |
| Number (no decimal) | int64 / float64 | Depends on decimal point |
| Number (decimal) | float64 | |
| Boolean | bool | |
null | pointer 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:
- Open JSON to Go.
- Paste your JSON (object or array).
- Configure:
- JSON Tag: default Keep original keys so
json:"…"matches the sample wire shape; choosesnake_case/camelCaseonly 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 optionalomitempty.
- JSON Tag: default Keep original keys so
- Output updates as you type—copy the
structcode into your project. - 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.Timeand 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.
Related Tools
Related Articles
Online CSS Minifier: Reduce CSS File Size for Better Performance
Learn how to minify CSS safely: whitespace, comments, hex shortening, zero-unit elision, calc protection, and UTF-8 byte savings — with an online tool you can use locally.
Regex Not Matching? Troubleshoot These 6 Common Pitfalls
How to troubleshoot when a regex doesn't match or matching fails? This article walks through 6 common pitfalls — greedy quantifiers, missing flags, unescaped special chars, lookaround assertions, newline handling, and Unicode properties — with before/after examples to help you quickly locate regex debugging issues.
How to Fix 'Hydration failed / Text content does not match' in Next.js
The complete guide to Next.js/React SSR hydration errors: why server and client renders diverge, the three usual suspects (timestamps, random values, localStorage), correct suppressHydrationWarning usage, and a ClientOnly pattern for browser-only widgets.