How to parse JSON into a struct in Go
Declare a struct with exported fields and json:"…" tags, then hand json.Unmarshal a pointer to it: json.Unmarshal(data, &u). It returns an error — always check it. Keys the struct doesn't declare are ignored, and fields the JSON doesn't mention keep their zero value.
encoding/json is reflection-driven, so your struct is the schema: it decides which keys are read, what they are converted to, and what gets thrown away. Two rules cause most of the "why is my struct empty?" questions — the destination must be a pointer, and the fields must be capitalised, because the package literally cannot see unexported fields. Everything below runs on this page: hit Run, then edit the JSON and run it again. Going the other way? Convert a struct to JSON.
1json.Unmarshal into a struct pointerRecommended
The signature is json.Unmarshal(data []byte, v any) error. Pass &u, not u — Unmarshal has to write through a pointer, and a non-pointer argument compiles fine but fails at runtime with an InvalidUnmarshalError. The json:"name" tag maps the JSON key on the left to the Go field on the right; that is the entire contract.
Output
Prints Ada, [email protected], 37, then {Name:Ada Email:[email protected] Age:36 Active:true} — Age is a real int, so u.Age + 1 just works, no cast needed. One subtlety worth knowing: Unmarshal does not reset the destination first. Decoding {"name":"new"} into a struct that already held {old 7} leaves Age at 7, so reuse a fresh var u User per payload unless you actually want that merge.
2Struct tags and how fields get matched
Matching happens in three passes: an exact tag match wins, then an exact field-name match, then a case-insensitive field-name match. That last pass is why "TITLE" lands in a field called Title with no tag at all. A tag of json:"-" excludes the field entirely, unexported fields are invisible, and any JSON key with nowhere to go is silently discarded.
Output
Output: GO-1, Gopher plush, 24.5, then "" and 0. Internal stays empty even though the JSON supplied it, because json:"-" removes the field from the mapping; stock stays 0 because it is lowercase. And "discount" vanished without a word — the default decoder never complains about keys it doesn't recognise, which is exactly the failure mode section 6 fixes.
3JSON arrays and nested objects
A top-level JSON array decodes into a []T — same call, you just point it at a slice instead of a struct, and Unmarshal grows it for you. Nested objects are a nested struct type (or a named type used as a field), and once decoded there is nothing left to unwrap: it is ordinary field access, checked by the compiler.
Output
Prints count: 2, then one line per repo — 0 gopher 5 stars Berlin/DE tags=[cli go] and 1 ziggy 3 stars Lisbon/PT tags=[] — followed by Berlin, cli and true 0. That last line is the distinction people trip on: [] in the JSON gives you an empty but non-nil slice, while a missing "tags" key would leave it nil. Both have len 0, so len(s) == 0 is the safe test either way.
4Unknown shapes: map[string]any
When you genuinely don't know the shape at compile time, decode into map[string]any. You trade every compile-time guarantee for type assertions, and you inherit the trap that catches everyone: every JSON number becomes a float64, so m["id"].(int) panics even when the JSON said 42. Ranging a map is also unordered, so sort the keys before printing.
Output
The printed table covers five of the six things a JSON value can become when the target is any: bool, <nil> for null, string, []interface {} for arrays, and float64 for every number — note id reports float64 42, not an int. (A nested object would be the sixth, map[string]interface {}.) Then the damage: 9007199254740993 prints back as 9.007199254740992e+15 — a float64 has 53 bits of mantissa and that integer needs 54, so it is silently rounded. dec.UseNumber() stores the untouched digits as a json.Number instead, and n.Int64() recovers 9007199254740993 exactly. Prefer a struct whenever you can; see also checking whether a key exists before asserting on it.
5Telling "missing" apart from "zero"
A plain bool field can't answer "did the client send false, or send nothing?" — both leave it false. Make the field a pointer and the two states separate: nil means the key was absent, non-nil means it was present and you can read the value through it. This is the standard way to write a correct PATCH handler or a config overlay.
Output
The first payload reports explicitly false and explicitly 0; the second reports absent for both. Note the third case: an explicit "notify": null also lands as nil, so pointers distinguish absent-or-null from present, not absent from null. If you must tell null apart from a missing key too, keep the field as json.RawMessage and inspect the raw bytes, or give the type a custom UnmarshalJSON.
6Strict decoding and useful error messages
json.NewDecoder(r).Decode(&v) reads from an io.Reader — an HTTP body, a file — without buffering the whole thing first, and it unlocks DisallowUnknownFields(), which turns a typo like "prot" into a real error instead of a mystery zero value. On the way out, encoding/json returns typed errors, so errors.As lets you tell "this isn't JSON" from "this field is the wrong type" and report the offending field by name.
Output
Five payloads, five outcomes: ok: {Host:db.local Port:5432}; field "port" wants int, got JSON string; malformed JSON at byte 20: invalid character '"' after object key:value pair; truncated: the body ended mid-value; and rejected: json: unknown field "prot". Two things to file away — a truncated document yields io.ErrUnexpectedEOF, not a *json.SyntaxError, so handle it separately; and the unknown-field error is a plain error with no dedicated type, so match it last. Also note UnmarshalTypeError hands you Field, Type, Value and Offset as fields, which is far better material for an API error response than dumping err.Error() at the caller.
7Which should you use?
| Approach | Type safety | Unknown keys | Best for |
|---|---|---|---|
| json.Unmarshal(data, &v) | Full — compile-checked | Dropped silently | Payloads whose shape you know |
| var s []T | Full | Dropped silently | Top-level JSON arrays |
| map[string]any | None — type assertions | All kept | Shape unknown at compile time |
| *bool / *int fields | Full, plus missing ≠ zero | Dropped silently | PATCH bodies, config overlays |
| Decoder + DisallowUnknownFields | Full | Rejected with an error | Config files, strict APIs, streams |
| json.RawMessage | Deferred to a second pass | Preserved verbatim | Polymorphic or versioned payloads |
Frequently asked questions
Why is my Go struct empty after json.Unmarshal?
Almost always one of two things. Either the fields are lowercase — encoding/json uses reflection and cannot write unexported fields, so name string is invisible while Name string works — or you passed the struct by value instead of by address. json.Unmarshal(data, u) compiles but returns an InvalidUnmarshalError at runtime and fills in nothing; you need json.Unmarshal(data, &u). Check the returned error and both problems announce themselves immediately.
Do I need json struct tags to parse JSON in Go?
No. Without a tag, encoding/json matches the JSON key against the field name exactly first, then case-insensitively, so a key of "title", "Title" or "TITLE" all fill a field named Title. You need tags when the JSON name cannot be a Go field name (user_id, created-at), when you want a different name than the field, or when you want json:"-" to exclude a field entirely. Tags are also self-documenting, which is why most Go codebases write them anyway.
How do I parse a JSON array into a slice in Go?
Point json.Unmarshal at a slice: var repos []Repo then json.Unmarshal(data, &repos). The decoder allocates and grows the slice for you, so there is nothing to size up front. It works for []string, []int and slices of structs alike, and a nested array inside an object is just a slice-typed field such as Tags []string carrying a tags tag. An empty JSON array gives you a non-nil slice of length 0, while a missing key leaves the field nil — len(s) == 0 is true for both.
Why does my JSON number come out as a float64 in Go?
Because when the destination is any (for example map[string]any), the decoder has no type to aim at and JSON has only one number type, so every number becomes a float64 — which is why m["id"].(int) panics. Fix it by decoding into a struct with a real int field, or by calling dec.UseNumber() on a json.Decoder, which stores numbers as json.Number (the original text) so you can call Int64(), Float64() or String(). UseNumber also protects integers above 2^53, which silently lose precision as float64.
How do I tell a missing JSON field from a zero value in Go?
Declare the field as a pointer — Notify *bool — so nil means "the key was not there" and a non-nil pointer means the value was sent, including false or 0. Note that an explicit null in the JSON also decodes to nil, so pointers separate absent-or-null from present. If you must distinguish null from a missing key, use json.RawMessage for the field and inspect the raw bytes, or implement a custom UnmarshalJSON on a wrapper type that records that it was called.
How do I make Go reject unknown JSON fields?
Use a decoder rather than json.Unmarshal: dec := json.NewDecoder(r) then dec.DisallowUnknownFields() before dec.Decode(&v). Any key with no matching struct field then produces an error such as json: unknown field "prot" instead of being dropped. That is the right default for config files and internal APIs where a typo should fail loudly; leave it off for public APIs that must tolerate clients sending newer fields. json.Decoder also streams from an io.Reader, so it suits HTTP request bodies and large files.