How to Convert JSON to Go Structs (Tags, Pointers, and omitempty)
Go’s encoding/json needs a concrete type to unmarshal into, so working with any JSON API in Go starts with writing a struct. For a large payload that is slow, and the json struct tags that map exported fields back to their lowercase keys are easy to get wrong. Generating the structs from a real sample fixes both. This guide covers how the mapping works, the fastest way to do it, and the Go-specific gotchas around tags, pointers, and omitempty.
The fastest route: a browser generator
For a one-off, paste the JSON into the JSON to Go converter. It reads the sample, infers the type of every field, and writes structs with a json tag on each one: nested objects become their own structs, arrays become slices, numbers resolve to int64 or float64, and the output is column-aligned the way gofmt lays it out. Because it runs entirely in your browser, the JSON never leaves your machine, which matters when the sample is a real production response.
If the JSON will not convert, it is almost always invalid rather than genuinely unconvertible. Run it through the JSON parser to find the exact syntax error, or format it so you can see the structure first.
How JSON maps to Go
A generator makes the same decisions every time. Knowing them tells you what to expect.
- Objects become structs. Each object turns into a
struct, and each key becomes an exported field in PascalCase with ajsontag carrying the original key. - Nested objects get their own struct. Rather than inlining a deep object, the generator names it and references it from the parent, which keeps the types reusable.
- Arrays become slices.
["a","b"]becomes[]string; an array of objects becomes[]Itemwith a generatedItemstruct. - Numbers split into
int64andfloat64. Whole numbers map toint64; anything with a decimal point maps tofloat64.
Here is the shape of a simple conversion:
{ "id": 1, "name": "Ada", "tags": ["admin", "dev"] }
becomes:
type Root struct {
ID int64 `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags"`
}
Note the ID field: the generator uppercases Go’s common initialisms (Id becomes ID, Url becomes URL) so the output passes golint without hand-editing.
Why the json tag matters
Go exports a field only if its name starts with a capital letter, so encoding/json can only populate exported fields. But JSON keys are usually lowercase. The json tag bridges that gap:
UserID int64 `json:"userId"`
Without the tag, encoding/json would look for a key named UserID and silently leave the field at its zero value. This is the single most common JSON-in-Go bug, and it is exactly what generation removes: every field gets the right tag automatically.
Optional fields and pointers
Two Go-specific cases deserve attention, and both are inferred from the data.
omitempty is added to the tag when a key is missing from some objects in an array. Paste an array of several records and the generator can see which keys are not always present:
Qty int64 `json:"qty,omitempty"`
Pointers handle nullability. A plain int64 cannot be nil, so it cannot tell “zero” apart from “absent”. When a field is sometimes null in the sample, the generator makes it a pointer so nil is representable:
Coupon *string `json:"coupon,omitempty"`
This is why you should paste a representative sample with several records and with nullable fields actually showing null: a single clean object cannot reveal optionality or nullability.
When you see interface{}
A field that is only ever null, an empty array, or holds more than one unrelated type has no single concrete Go type, so it falls back to interface{} (or []interface{} for a mixed array). That is not a bug, it is a lack of evidence. Populate those fields in the sample to get precise types, and reach for the JSON viewer to confirm the shape of a deeply nested payload before you convert.
Doing it in code
For a repeatable build step rather than a one-off, two tools are common:
json-to-go(the web tool by Matt Holt, also a library) generates Go structs from JSON and is the de facto standard for this exact task.easyjsonorffjsongenerate fast, reflection-free marshaling code once you already have the structs, which is worth it on hot paths.
And the standard-library baseline, once you have a struct, is simply:
var order Root
if err := json.Unmarshal(data, &order); err != nil {
log.Fatal(err)
}
Generate the struct from a sample, drop it in, and Unmarshal does the rest.
The gotchas that bite
- Missing or wrong tags. Without a
jsontag, a field stays at its zero value. Generation gets the tags right, but if you hand-edit, keep them in sync with the API. - int64 vs float64 for IDs. A large numeric ID that exceeds what
float64represents exactly will lose precision if any value in the sample forced the field tofloat64. If an ID can be huge, have the API send it as a string. - Zero value vs absent.
omitemptyomits zero values on marshal, which is not always what you want. A real0orfalseyou intend to send will be dropped; use a pointer when the distinction matters. - One sample hides optionality. A single object makes every field look required. Feed an array of several records so the generator can add
omitemptywhere it belongs. - Enums become plain strings. A field that is really one of a few values infers as
string; define your own typed constants if you want compile-time safety.
Frequently asked questions
How do I convert JSON to a Go struct?
Paste the JSON into the JSON to Go converter and it writes the structs with json tags, inferring nested types, slices, int64/float64, pointers, and omitempty. In a build step, the json-to-go library does the same.
Why is my Go struct field empty after unmarshaling?
Almost always a missing or wrong json tag, or an unexported (lowercase) field name. encoding/json only fills exported fields, and it matches keys via the tag. Generating the struct gives every field the correct tag.
When should a field be a pointer?
When you need to tell null or absent apart from the zero value. A *int64 can be nil; a plain int64 cannot distinguish a missing value from 0. The generator uses a pointer when the sample shows the field as null.
What does omitempty do?
On marshal, it omits the field when it holds its zero value (empty string, 0, false, nil). It is useful for optional fields, but be careful: a legitimate 0 or false will also be dropped.
For the format underneath all this, see the guide on what JSON is, the same idea for the front end in generating TypeScript interfaces from JSON, and the roundup of free JSON tools for the rest of the toolkit.
Leave a Reply