Why Generate Go Structs From JSON?
Go's encoding/json package needs a target type to Unmarshal into. You can decode into map[string]interface{}and type-assert every access, but that discards all of Go's compile-time safety and makes code twice as long. The idiomatic path is a typed struct with json tags — and typing one by hand from a 50-field API response is a time sink. Generating it from a real sample takes a second and keeps your code type-safe.
Example: API Response to Struct
Given this JSON from GET /users/42:
{
"id": 42,
"name": "Ada Lovelace",
"email": "[email protected]",
"verified": true,
"roles": ["admin", "editor"],
"profile": {
"bio": "Mathematician",
"avatar": null,
"website": "https://ada.dev"
},
"created_at": "2026-10-03T12:00:00Z"
}The generator produces:
type Profile struct {
Bio string `json:"bio"`
Avatar *string `json:"avatar"`
Website string `json:"website"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Verified bool `json:"verified"`
Roles []string `json:"roles"`
Profile Profile `json:"profile"`
CreatedAt string `json:"created_at"`
}Go Type Mapping Reference
How JSON types map to Go types in the output:
"abc"→string42→int(fits int32) orint64(larger)3.14→float64true/false→boolnull→*T(pointer so nil-vs-zero is representable)[1, 2]→[]int{...}→ named nested struct- Mixed array →
[]interface{}(or[]anyin Go 1.18+)
Pointers vs omitempty — When to Use Each
These two features look similar but solve different problems.
Pointers (*string, *int) let a field be truly absent at runtime. nilmeans "this JSON key was missing or null"; a non-nil pointer means a value was present. Without the pointer, Name string defaults to "" whether the key was missing, null, or an empty string — three different states collapsed into one.
omitempty in the json tag is a Marshal-time flag. When you serialize the struct back to JSON, omitempty drops any field whose Go value equals the zero value. Pair it with pointers to preserve round-trip accuracy: nil pointer + omitempty = key is omitted from output.
type Patch struct {
Name *string `json:"name,omitempty"` // present → send, nil → skip
Email *string `json:"email,omitempty"`
Age *int `json:"age,omitempty"`
}
// json.Marshal(Patch{Name: strPtr("Ada")})
// → {"name":"Ada"} (email, age skipped)
// json.Marshal(Patch{Name: strPtr("")})
// → {"name":""} (empty string IS sent)Handling Numbers Larger Than 2^53
Go has first-class support for 64-bit integers, but JavaScript (and therefore most JSON producers) do not — numbers above Number.MAX_SAFE_INTEGER (2^53-1) lose precision. Twitter, Discord, and many database APIs now return IDs as strings for this reason. If your JSON looks like "id": "1759502348000000000", type it as string in Go and parse with strconv.ParseInt where needed.
Custom UnmarshalJSON for Flexible Fields
Some APIs return the same field as either a string or a number depending on the day of the week. Rather than fighting it with interface{}, write a custom UnmarshalJSON:
type FlexInt int64
func (f *FlexInt) UnmarshalJSON(data []byte) error {
// Try as number first
var n int64
if err := json.Unmarshal(data, &n); err == nil {
*f = FlexInt(n)
return nil
}
// Fall back to string
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsed, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return err
}
*f = FlexInt(parsed)
return nil
}
type Payload struct {
ID FlexInt `json:"id"` // accepts 42 or "42"
}Common Pitfalls
- Unexported fields are invisible to
encoding/json. The generator uppercases every field name (ID, Name) so Go's reflection can read them. If you rename a field to lowercase manually, Unmarshal silently ignores it. - Tag typos fail silently.
`json:"nam"`(missing e) just means thenamekey stays zero-valued. UseDisallowUnknownFieldson your decoder to catch mismatches. - Time fields need
time.Timeor a wrapper. The generator emits ISO-8601 timestamps asstringbecause that always works. Change totime.Timeif you want direct parsing — Go's standard library handles RFC 3339. - Empty slice vs nil slice.
json.Marshalof anilslice emitsnull; an empty[]string{}emits[]. If your API expects an empty array, initialise the slice.
Related JSON Tools
- JSON Formatter — the parent tool.
- JSON to TypeScript Interface — same concept for TS projects.
- Validate JSON Online — check the JSON parses cleanly before generating types.
- JSON Schema Validator — formal schema checking.