This page is the deep reference for json.Unmarshal in encoding/json: how the API works, how to target structs, maps, slices, and any, what null does, when to use json.RawMessage, and the errors people hit in practice. For reading JSON from disk or HTTP, streaming with json.Decoder, MarshalIndent, and the broader “parse JSON in Go” path, use Parse JSON in Go. For omitting fields on encode, see JSON omitempty; for struct basics, see structs in Go.
Tested on: Go go1.24.4 linux/amd64; kernel 6.14.0-37-generic.
What does json.Unmarshal do in Go?
json.Unmarshal converts JSON text (as a Go []byte, usually UTF-8) into a Go value: a struct, map, slice, string, number, boolean, or a tree built from any (interface{}). The decoder walks the JSON value and writes into the variable you provide. It does not read files or HTTP bodies by itself—you pass the bytes you already loaded (see the parse JSON guide for os.ReadFile and response bodies).
json.Unmarshal syntax
The signature is:
func Unmarshal(data []byte, v any) errorThe first argument is the raw JSON as []byte. The second is the destination; in real code you pass a pointer such as &person or &m so Unmarshal can modify the value. The return value is an error: non-nil for invalid JSON, wrong JSON kind (object vs array), type mismatches, or an invalid destination (for example not a pointer). Always check it.
Unmarshal JSON into a struct
The usual case is a JSON object into a struct whose exported fields line up with the keys. Field names match JSON keys by default (case-sensitive); you add `json:"name"` tags when the wire format uses different spellings.
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `json:"name"`
Score float64 `json:"score"`
}
func main() {
data := []byte(`{"name":"Harry Potter","score":9.5}`)
var s Student
if err := json.Unmarshal(data, &s); err != nil {
panic(err)
}
fmt.Printf("%+v\n", s)
}After Run, you should see the struct populated with Name and Score (for example Name:Harry Potter Score:9.5 in the default %+v shape).
Exported fields are required
Only exported (capitalized) struct fields can receive JSON values. Lowercase fields are invisible to encoding/json, so they stay at the zero value and can make it look like “half the struct decoded.”
Use tags when JSON names differ
When the API uses first_name, user_id, created_at, or camelCase that does not match your Go field names, set tags such as First string `json:"first_name"` so the decoder knows which JSON key maps to which field.
Why fields are empty after Unmarshal
If the call returns nil but fields look wrong, typical causes are:
- Unexported fields (lowercase names) so JSON never applies.
- Missing or wrong
jsontags so keys do not match. - Wrong nesting: JSON has a flat key but you put the field inside an embedded struct without matching the object shape.
- Unmatched keys: JSON uses
userNamebut the struct hasUsernamewithout a tag. - Wrong destination type: decoding an object into a string, and so on—often still an error, but some paths zero the target.
- Ignored
error: the decode failed earlier and the variable was partially untouched.
This program shows two typical cases: an unexported field never receives the value even when the JSON key matches the tag, while the exported tagged field does. A second struct uses a tag that does not match the JSON key, so the field stays empty.
package main
import (
"encoding/json"
"fmt"
)
type Broken struct {
id string `json:"id"` // unexported: json package ignores this field
Name string `json:"name"`
}
type WrongTag struct {
UserID string `json:"user_id"` // JSON has "userId", not "user_id"
}
func main() {
raw := []byte(`{"id":"42","name":"Ada"}`)
var b Broken
if err := json.Unmarshal(raw, &b); err != nil {
panic(err)
}
fmt.Printf("Broken: id %q Name %q\n", b.id, b.Name)
raw2 := []byte(`{"userId":"7"}`)
var w WrongTag
if err := json.Unmarshal(raw2, &w); err != nil {
panic(err)
}
fmt.Printf("WrongTag: UserID %q\n", w.UserID)
}After Run, Broken.id stays empty while Name is Ada, and WrongTag.UserID stays empty because the JSON key does not match the tag.
The pointer section below shows another frequent cause of “nothing changed.”
Pointer requirement in json.Unmarshal
Unmarshal must receive a pointer because it writes into existing memory. Passing a non-pointer value means the function would only mutate a copy, which is not how the API is designed—so encoding/json rejects most non-pointer destinations with InvalidUnmarshalError.
Pass &v for a struct, &m for a map, &s for a slice, and &x for a scalar box when you decode into int, string, or similar. A common mistake is json.Unmarshal(data, person) instead of json.Unmarshal(data, &person).
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
}
func main() {
data := []byte(`{"name":"Lin"}`)
var p Person
if err := json.Unmarshal(data, p); err != nil {
fmt.Println("non-pointer:", err)
}
if err := json.Unmarshal(data, &p); err != nil {
panic(err)
}
fmt.Println("pointer:", p.Name)
}The first call should print a json: Unmarshal(non-pointer ...) style error; the second call succeeds and prints the name.
Collections and nesting
Unmarshal JSON into a map
For dynamic objects whose keys are not fixed at compile time, decode into map[string]any (legacy code often used map[string]interface{}). Values come back as concrete dynamic types; in particular JSON numbers decode as float64 inside any, which matters for IDs and large integers.
package main
import (
"encoding/json"
"fmt"
)
func main() {
data := []byte(`{"id":123,"name":"GoLinux Cloud","phoneNumber":1234567890}`)
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
panic(err)
}
fmt.Printf("id type %T value %v\n", m["id"], m["id"])
if name, ok := m["name"].(string); ok {
fmt.Println("name:", name)
}
}After Run, the numeric entries report as float64. For files, streams, and UseNumber, see Parse JSON in Go.
Unmarshal JSON array into a slice
JSON arrays map to Go slices. Element types follow the usual rules: []User for an array of objects, []string for string arrays, []int when every element is a JSON number that fits int, or []map[string]any for heterogeneous objects.
package main
import (
"encoding/json"
"fmt"
)
type Item struct {
ID string `json:"id"`
}
func main() {
raw := []byte(`[{"id":"a"},{"id":"b"}]`)
var items []Item
if err := json.Unmarshal(raw, &items); err != nil {
panic(err)
}
fmt.Println(len(items), items[0].ID, items[1].ID)
}You should see length 2 and the two IDs printed.
Unmarshal nested JSON
Nested JSON objects and arrays decode into nested structs, pointers to structs, or nested maps/slices, as long as the Go shape mirrors the JSON hierarchy. Tags apply at each level.
package main
import (
"encoding/json"
"fmt"
)
type Batter struct {
ID string `json:"id"`
Type string `json:"type"`
}
type Cake struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Ppu float64 `json:"ppu"`
Batters struct {
Batter []Batter `json:"batter"`
} `json:"batters"`
}
func main() {
const raw = `{"id":"0001","type":"donut","name":"Cake","ppu":0.55,"batters":{"batter":[{"id":"1001","type":"Regular"},{"id":"1002","type":"Chocolate"}]}}`
var c Cake
if err := json.Unmarshal([]byte(raw), &c); err != nil {
panic(err)
}
fmt.Println(c.Type, c.Name, len(c.Batters.Batter))
}You should see the cake type, name, and a batter count of 2.
Unmarshal JSON into any or interface{}
If you decode into any (or a pointer to any) without a concrete struct, encoding/json picks dynamic types:
| JSON kind | Go type after decode into any |
|---|---|
| object | map[string]any |
| array | []any |
| string | string |
| number | float64 |
| boolean | bool |
| null | nil |
Nested values follow the same rules recursively.
Decode a few top-level JSON values into var v any and inspect the dynamic Go type of each:
package main
import (
"encoding/json"
"fmt"
)
func main() {
cases := []string{
`{"a":1,"b":[true,null]}`,
`[1,"x",{}]`,
`3.5`,
`"plain"`,
`true`,
`null`,
}
for _, s := range cases {
var v any
if err := json.Unmarshal([]byte(s), &v); err != nil {
panic(err)
}
fmt.Printf("%-22s -> %T %#v\n", s, v, v)
}
}You should see an object become map[string]any, an array become []any, a number become float64, a string stay string, a boolean stay bool, and top-level null become a nil interface value.
Handling null values
JSON null maps to Go as follows in typical cases:
- Pointer fields (
*int,*string,*time.Time, …):nullsets the pointer tonil; a JSON value allocates and sets the pointed-to value. any/interface{}:nullbecomes untypednil.- Maps, slices, interfaces:
nullgenerally clears or sets the slot to a nil map/slice/interface per assignment rules for the destination type. - Non-pointer value fields (for example
int,string): JSONnullis ignored—the field keeps whatever Go value it already had (often zero if unset) andUnmarshalreturns no error (encoding/jsondocs: null only applies to interface, map, pointer, or slice). Use a pointer orjson.Unmarshalerif you need to branch on JSONnullexplicitly.
For optional API fields, pointers or json.RawMessage are common so you can tell “missing” vs “present but null” when you need that distinction.
package main
import (
"encoding/json"
"fmt"
)
type WithPointer struct {
Count *int `json:"count"`
}
type WithValue struct {
Count int `json:"count"`
}
func main() {
var p WithPointer
if err := json.Unmarshal([]byte(`{"count":null}`), &p); err != nil {
panic(err)
}
fmt.Println("pointer field is nil:", p.Count == nil)
var v WithValue
v.Count = 99
if err := json.Unmarshal([]byte(`{"count":null}`), &v); err != nil {
panic(err)
}
fmt.Println("int after JSON null (unchanged):", v.Count)
}After Run, the pointer struct should report true for p.Count == nil. For a plain int, encoding/json ignores JSON null—the field keeps its existing Go value and Unmarshal returns nil error—so Count stays 99 here. Use *int (or json.Unmarshaler) when you must distinguish JSON null from a numeric zero.
Partial JSON parsing with json.RawMessage
json.RawMessage is an alias of []byte that implements UnmarshalJSON by storing the raw sub-document. Use it when one subtree should be decoded later—often after reading a type, kind, or op discriminator in the same object.
package main
import (
"encoding/json"
"fmt"
)
type Envelope struct {
Kind string `json:"kind"`
Data json.RawMessage `json:"data"`
}
func main() {
raw := []byte(`{"kind":"greeting","data":{"msg":"hello"}}`)
var env Envelope
if err := json.Unmarshal(raw, &env); err != nil {
panic(err)
}
var payload struct {
Msg string `json:"msg"`
}
if err := json.Unmarshal(env.Data, &payload); err != nil {
panic(err)
}
fmt.Println(env.Kind, payload.Msg)
}You should see the kind and message printed together.
Strict field checking
json.Unmarshal ignores unknown object keys by default: extra keys in the JSON do not produce an error. That is convenient for forward-compatible clients but weak for strict request validation.
There is no flag on Unmarshal itself to change that. For strict decoding, use json.Decoder with DisallowUnknownFields and Decode from a byte reader (still standard library). Full patterns for APIs and files live in Parse JSON in Go.
package main
import (
"encoding/json"
"fmt"
"strings"
)
func main() {
type V struct {
A int `json:"a"`
}
dec := json.NewDecoder(strings.NewReader(`{"a":1,"extra":2}`))
dec.DisallowUnknownFields()
var v V
err := dec.Decode(&v)
fmt.Println(err)
}You should see a non-nil error that mentions an unknown field (extra).
Custom UnmarshalJSON (advanced)
Types can implement UnmarshalJSON([]byte) error to control decoding—for example dates in non-RFC3339 layouts, enums from strings, or numbers sent as quoted strings. Keep implementations tight: return an error on invalid input so callers do not silently get zero values.
package main
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
type Port int
func (p *Port) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
n, err := strconv.Atoi(strings.TrimSpace(s))
if err != nil {
return err
}
*p = Port(n)
return nil
}
type Host struct {
Addr string `json:"addr"`
Port Port `json:"port"`
}
func main() {
raw := []byte(`{"addr":"127.0.0.1","port":"8080"}`)
var h Host
if err := json.Unmarshal(raw, &h); err != nil {
panic(err)
}
fmt.Println(h.Addr, int(h.Port))
}You should see the host and numeric port decoded from the string JSON value.
Marshal vs Unmarshal
json.Marshalconverts a Go value into JSON[]byte(pluserror).json.Unmarshalconverts JSON[]byteinto a Go value you point at.
They share struct tag semantics for names and omitempty on marshal. Pretty-printed output such as MarshalIndent is orthogonal to Unmarshal; use it when generating human-readable JSON, not when explaining decode rules.
Unmarshal vs Decode
- Use
json.Unmarshalwhen the full JSON document is already in memory as[]byteorstringyou converted with[]byte(s). - Prefer
json.NewDecoder(r).Decode(&v)when reading from anio.Reader(file, HTTP body,bytes.Reader,strings.Reader) so you can stream, setUseNumber, or callDisallowUnknownFieldson the decoder.
Details and examples are centralized in Parse JSON in Go.
Common json.Unmarshal errors
json: Unmarshal(non-pointer ...)
Returned when the second argument is not a pointer to a concrete value (or is an illegal target). Fix by passing &v for your struct, map, slice, or boxed scalar.
Type mismatch errors
Examples: JSON string into an int field, JSON object where the destination is a slice, or JSON array where the destination is a struct. The error text usually names the path into the JSON; read it alongside the struct tags. (JSON null into a plain value field is not an error—see the null section above.)
Fields remain empty
Revisit exported fields, json tags, nesting, and the pointer requirement. Confirm err == nil after the call. If keys are correct but types are wrong, you may get an error instead of silent empties—depends on the branch.
Real-world scenarios, best practices, and cheat sheet
Short mapping from situation to approach:
| Scenario | Typical target |
|---|---|
| Known API or config schema | Struct with json tags |
| Unknown keys at compile time | map[string]any + assertions |
| List endpoint returns a JSON array | Slice of struct or []map[string]any |
| Optional or nullable fields | Pointers, json.RawMessage, or custom UnmarshalJSON |
| Envelope + varying inner payload | Outer struct + json.RawMessage + second Unmarshal |
| Strict request body validation | Decoder + DisallowUnknownFields + Decode |
| Large file or HTTP body | Decoder on a reader (see parse JSON) |
Practices that stay true for most services: prefer structs for stable schemas, maps for truly dynamic trees, always check errors, keep fields exported, use tags for wire names, validate required business fields after a successful decode, and reach for Decoder when input is a stream or you need decoder-only options.
Compact cheat sheet:
| Goal | Use |
|---|---|
| Object with known shape | Struct + &v |
| Arbitrary object | map[string]any (remember float64 for numbers) |
| JSON array | []T or []any |
| Nested document | Nested struct or recursive maps |
| Fully dynamic top-level | var v any then type switch |
Optional / null |
Pointers or custom unmarshaler |
| Delay part of tree | json.RawMessage |
| Reject unknown keys | Decoder.DisallowUnknownFields |
| Custom wire format for one field | UnmarshalJSON on a named type |
| Bytes already in RAM | json.Unmarshal |
| Reader / stream / HTTP | json.NewDecoder(r).Decode |
Summary
json.Unmarshal turns a []byte JSON document into Go data through a pointer you supply, using struct tags for wire names and only touching exported fields. Maps and any give flexible trees at the cost of float64 numbers and type assertions; slices and nested structs mirror JSON arrays and objects. JSON null and optional fields are easiest to model with pointers or raw sub-documents. Unknown JSON keys are ignored on Unmarshal itself—tight validation belongs to json.Decoder options or post-decode checks. json.Marshal is the symmetric encode; json.Decoder fits readers and advanced decode flags. For files, HTTP, streaming, and end-to-end parsing workflows, stay on Parse JSON in Go and keep this article as the Unmarshal-behavior reference.
References
encoding/jsonpackagejson.Unmarshaljson.Marshaljson.Decoder- JSON and Go (blog)
- Snippet verifier in this repo:
examples/golang-json-unmarshal-verify(rungo test -vthere to match the article’spackage mainexamples).

