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.

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:

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

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

Your email address will not be published. Required fields are marked *