Converting JSON to a Go struct means turning a sample JSON payload into a Go type with the right field types, exported names, and json: tags so you can Unmarshal into it cleanly. A generator does this by inspecting the value of each key and inferring a Go type, then mapping the JSON key name to a PascalCase exported field with a matching tag. This guide shows exactly how that inference works, where it goes wrong, and how to get a correct struct without hand-typing a single brace.
Most developers hit the same wall: an API returns a 40-key JSON object, and writing the matching struct by hand is slow and bug-prone. You forget a tag, guess int where the API sends a float, or flatten a nested object that should be its own type. The result is a runtime Unmarshal that silently drops fields. The fix is understanding the rules a converter applies, so you can trust its output and fix the cases it cannot guess.
How a JSON to Go Struct Converter Infers Types#
A converter walks the JSON value by value. JSON only has six value types (string, number, boolean, object, array, null), but Go has many. The mapping is mostly deterministic, with two genuinely ambiguous cases that you need to know about.
Here is the core mapping every generator uses:
| JSON value | Inferred Go type | Notes |
|---|---|---|
"hello" | string | Always unambiguous |
true / false | bool | Always unambiguous |
42 | int | Whole numbers default to int |
3.14 | float64 | Any decimal becomes float64 |
{ ... } | nested struct | Becomes its own named type |
[ ... ] | []T | Element type inferred from items |
null | interface{} | No type information available |
The two cases worth your attention are numbers and null. Everything else the converter gets right every time.
Why int vs float64 is the trickiest call#
JSON has no integer type. The number 5 and the number 5.0 are both just "number" to a JSON parser. A generator has to guess: if the literal has no decimal point, it picks int; if it sees a decimal point, it picks float64.
That heuristic breaks in a common way. Imagine an API field price that happens to be 100 in your sample but is usually 100.50. The converter sees a whole number and writes int. The next response with 100.50 then fails to unmarshal into an int, or worse, truncates.
Always sanity-check numeric fields against the API docs, not just your one sample. If a field can ever hold a fraction, change the generated
inttofloat64by hand. A single clean sample is not a schema.
There is also the json.Number option. If precision matters (large IDs, money, anything you cannot afford to round), decode into json.Number or a string and parse deliberately. Generators default to float64 for decimals, which is fine for most data but lossy for 64-bit integer IDs beyond 2^53.
When you get an empty interface type#
If a JSON value is null, the converter has nothing to infer from. There is no type, only the absence of one. So it falls back to interface{} (written any in modern Go).
The same fallback happens for mixed-type arrays. A JSON array like [1, "two", true] has no single element type, so the generator produces []interface{}. That is technically correct but awful to work with, because you lose all compile-time type safety and have to type-assert every element.
When you see interface{} in generated output, treat it as a flag that says "I could not figure this out, you decide." Usually the right move is to give the converter a richer sample where that field is populated, then regenerate.
Field Tags, Exported Names, and omitempty#
Getting the types right is half the job. The other half is the json: struct tags and field naming, which is where hand-written structs fail most often.
Why fields must be exported (capitalized)#
Go's encoding/json package can only read and write exported struct fields, meaning fields whose name starts with a capital letter. An unexported (lowercase) field is invisible to Unmarshal and Marshal. It will silently stay at its zero value with no error.
So a JSON key user_name cannot map to a Go field named user_name. The converter renames it to the exported UserName and adds a tag to bridge the gap:
type User struct {
UserName string `json:"user_name"`
Email string `json:"email"`
}
The tag json:"user_name" tells the JSON package: this exported UserName field corresponds to the JSON key user_name. Without that tag, Go would look for a key named UserName (it does a case-insensitive match as a fallback, but relying on that is fragile). The conversion from snake_case or camelCase to PascalCase plus an explicit tag is exactly what a good generator automates.
What omitempty actually changes#
The omitempty option appears in tags like json:"email,omitempty". It only affects marshaling (Go struct to JSON), never unmarshaling. When you encode a struct, omitempty tells the encoder to skip a field entirely if its value is the zero value (empty string, 0, false, nil, empty slice or map).
This matters for API output. Compare the same struct with and without it:
| Tag | Field value | JSON output |
|---|---|---|
json:"middle_name" | "" (empty) | "middle_name": "" |
json:"middle_name,omitempty" | "" (empty) | (field omitted) |
json:"age,omitempty" | 0 | (field omitted) |
The trap with omitempty is that it cannot tell "intentionally zero" from "not set." If age is legitimately 0 (a newborn), omitempty drops it from the output. When you need to distinguish "absent" from "zero," use a pointer field (*int) instead, where nil means absent and &zero means a real zero.
Most converters do not add
omitemptyby default, because it is a serialization choice, not a schema fact. Add it deliberately to fields that are genuinely optional in your output, and reach for pointer fields when the zero value is meaningful.
Handling Nested and Repeated Structures#
Real API responses are rarely flat. They nest objects inside objects and carry arrays of objects. This is where hand-writing structs gets genuinely painful and where a converter earns its keep.
When a converter meets a nested object, it has two strategies:
- Inline (anonymous) struct: the nested type is written inline inside its parent. Compact, but you cannot reference the inner type elsewhere.
- Named (extracted) types: each nested object becomes its own named
struct, referenced by the parent. More verbose, but reusable and far easier to read.
For anything beyond a trivial payload, prefer named types. Given this JSON:
{
"id": 7,
"owner": { "name": "Ada", "verified": true },
"tags": ["go", "json"]
}
A converter using named types produces:
type Repo struct {
ID int `json:"id"`
Owner Owner `json:"owner"`
Tags []string `json:"tags"`
}
type Owner struct {
Name string `json:"name"`
Verified bool `json:"verified"`
}
Notice tags became []string because every element was a string, and owner became its own Owner type. For an array of objects ("items": [ {...}, {...} ]), the converter inspects the first element (or merges across elements if it is smart) and produces []Item with a generated Item struct.
The array-merging edge case#
Here is a subtlety cheaper generators miss. If an array contains objects where some items have a field and others do not, a naive converter only reads the first element and misses fields that appear later. A robust generator merges keys across all elements of the array so the struct captures every field, marking ones that are sometimes absent as candidates for pointers or omitempty. If your generated struct is dropping fields that exist deeper in a list, that is the cause.
Generate a Go Struct From JSON, Step by Step#
You can do this in your browser in under a minute, no Go toolchain or go install required. Molixa's free JSON formatter and converter validates your JSON first, then emits a Go struct (alongside TypeScript, Zod, and other targets) so you catch syntax problems before they become Go compile errors.
Step 1: Paste and validate your JSON#
Drop a representative response into the editor. The most important word there is representative: use a sample with every field populated and realistic values, not a stripped-down one. Empty or null-heavy samples are exactly what produce interface{} fields. The formatter flags trailing commas, single quotes, and unquoted keys immediately, which is far less painful than chasing a Go unmarshal failure later.
Step 2: Generate the Go struct output#
Switch the output to the Go struct target. The converter walks your JSON, infers each type, exports and renames fields to PascalCase, and writes matching json: tags. Nested objects become named types and arrays become slices automatically. Copy the result straight into your package.
Step 3: Fix the ambiguous fields by hand#
This is the step generators cannot do for you, and skipping it is why people think converters are unreliable. Scan the output for three things:
intfields that can hold decimals: change tofloat64(orjson.Numberfor precision).interface{}fields: these came fromnullor mixed arrays. Replace with a concrete type, or feed a richer sample and regenerate.- Large integer IDs: if an ID exceeds 2^53, a
float64will lose precision. Useint64,string, orjson.Numberdepending on the API.
Step 4: Add omitempty and pointers where they belong#
Decide, per field, whether it is truly optional on output. Add ,omitempty to fields that should disappear when empty. For fields where a zero value is meaningful (a real 0, a real false), switch to a pointer type so nil cleanly signals "not provided." Then write a tiny Unmarshal test against your real sample to confirm nothing is silently dropped.
Trusting the Output: Verify Before You Ship#
A generated struct is a strong first draft, not a guarantee. The honest workflow is: generate, then prove it round-trips your real data. Unmarshal a real response into the struct, marshal it back, and diff against the original. Missing or reordered fields surface instantly.
If you also work in TypeScript on the same API, the same JSON sample can drive both. Our guide on converting JSON to a TypeScript interface covers the parallel type-inference rules on the front end, and if you are debugging the raw payload first, how to format JSON in JavaScript explains pretty-printing and the JSON.stringify options that make a messy response readable before you ever convert it.
The reason a converter beats hand-typing is not just speed. It applies the export rule, the tag mapping, and the nesting consistently every time, so the only thing left for you is the small set of judgment calls (int vs float, optional vs required, precision) that no tool can infer from a single sample. That division of labor is the whole point.
Conclusion: From Raw JSON to a Reliable Go Struct#
Converting JSON to a Go struct is mechanical for most fields and judgment-based for a few. A generator handles the mechanical 90%: exporting fields, writing json: tags, naming nested types, and turning arrays into slices. You handle the 10% it cannot infer, mainly numeric precision, null-derived interface{} fields, and whether omitempty or a pointer belongs on each optional field.
Get those rules right and the JSON to Go struct round-trip becomes boring in the best way. Paste a representative sample into the JSON formatter and Go struct converter, generate the type, fix the handful of ambiguous fields, and confirm it round-trips your real data. That is the difference between a struct that compiles and one you can actually trust in production.
Frequently Asked Questions#
How do I convert JSON to a Go struct?
Paste a representative JSON sample into a converter, which infers a Go type for each value, exports and renames keys to PascalCase, and adds matching json: tags. Then manually review numeric fields and any interface{} types the tool could not infer. You can do the whole thing free in the browser with Molixa's JSON formatter and converter, no Go toolchain needed.
Why does my generated struct have interface{} fields?
An interface{} (or any) field means the converter could not infer a type. This happens when the JSON value was null, or when an array contained mixed types. The fix is to provide a sample where that field is populated with a real value, then regenerate, or replace the interface{} with the concrete type you know it should be.
Do Go struct fields have to be capitalized?
Yes, if you want encoding/json to read or write them. Go's JSON package only sees exported fields, meaning those starting with a capital letter. A lowercase field is ignored during Marshal and Unmarshal with no error, so it silently stays at its zero value. Converters export every field and use the json: tag to map back to the original key name.
What does omitempty do in a JSON tag?
The omitempty option only affects marshaling (encoding a struct to JSON). It tells the encoder to skip a field when its value is the zero value: empty string, 0, false, nil, or an empty slice or map. It has no effect on unmarshaling. Be careful, because it cannot distinguish an intentional zero from an unset field, so use a pointer type when the zero value is meaningful.
Should I use int or float64 for JSON numbers in Go?
Use float64 for any field that can hold a decimal, and int only for values that are always whole. Because JSON has no integer type, converters guess from your sample, so a field that is 100 in the sample but sometimes 100.50 gets the wrong type. For large IDs or money where precision matters, prefer int64, string, or json.Number to avoid float rounding.
How do I handle nested JSON objects in a Go struct? Convert each nested object into its own named struct type referenced by the parent, rather than inlining everything. This keeps the code readable and lets you reuse the inner type. A good converter does this automatically and also handles arrays of objects by generating a slice of a named struct, ideally merging keys across all array elements so no field is missed.



