Understanding JSON → Go
Struct tags do most of the work.
How Go's encoding/json maps fields, why pointers exist for optionals, and the omitempty trap that catches every Go developer once.
The shape of a Go struct.
A Go struct generated from JSON looks like a record: exported field names (capitalised so the JSON package can see them), a Go type, and a backtick-quoted struct tag that tells encoding/json which JSON key it maps to. The tag is a string literal interpreted by the standard library; without it, Go would use the field name verbatim (which is usually wrong because JSON keys are typically snake_case).
Name string `json:"name"`
Pointers carry the "is this field present?" bit.
Go has no concept of undefined. Every value type has a zero value — 0 for int, "" for string, false for bool. A field of type int with value 0 is indistinguishable from a field that was missing from the JSON. To preserve that distinction, use a pointer: *int is nil when missing, points at zero when the JSON had "x": 0. The cost is the pointer dance everywhere the field is read; the benefit is correct null-vs-absent handling. Mainstream codegen tools choose between pointer and value based on whether the source schema marks the field optional.
The omitempty trap.
The struct tag suffix ,omitempty tells the encoder to drop the field if its value is the type's zero value. Useful for sending optional payloads. Dangerous as soon as zero is a meaningful value — a user setting their age to 0 (a fresh-faced baby), a counter at zero, an empty string that's legitimately empty. The fix is the pointer trick again: *int with omitempty omits only on nil, not on the underlying 0. Every Go developer hits this once and remembers it forever.
A worked example.
From {"id": 7, "name": "Q", "title": null, "tags": ["a"]}, a reasonable inferrer emits a struct with ID int `json:"id"`, Name string `json:"name"`, Title *string `json:"title"` (pointer because it was null), and Tags []string `json:"tags"` (slices are inherently nilable). The ID field gets the standard-library Go convention of an all-caps initialism.
JSON sample
{ "id": 7, "name": "Q", "title": null, "tags": ["a"] }
Each key maps to a struct field with a json tag.
title is null → pointer ; tags is array → []string slice
= ID/Name/Title *string/Tags []string
Time values.
The standard library marshals and unmarshals time.Time as RFC 3339 strings — the ISO 8601 subset most JSON APIs use. If your API returns Unix epoch seconds, you need a custom UnmarshalJSON method (or a wrapper type). Codegen tools often default to string for date-shaped fields and leave the conversion to the consumer; that's safer than guessing wrong and emitting time.Time on a value that doesn't parse.
Maps for dynamic keys.
A JSON object whose keys aren't fixed — a localisation table, a hashmap of feature flags, a per-day metric — should become a map[string]T not a struct. Codegen tools detect this when the same key set varies wildly between samples; if your tool emits an enormous struct with hundreds of fields, the source is probably better modelled as a map.
The decoder's strictness.
By default Go's JSON decoder is lenient: unknown fields in the input are silently ignored. For an internal API that's fine; for a public API or anything you want to catch regressions on, use json.NewDecoder(r).DisallowUnknownFields() which errors when the JSON has a key your struct doesn't. The codegen output is struct-only; the strictness is a runtime decoder choice.