Decoding JSON in Go
Parse JSON into Go structs with json.Unmarshal and json.NewDecoder, reject unknown fields, detect missing values, and decode HTTP request bodies safely.
Decoding is where JSON bugs hide. A typo in a field name gives you a zero value with no error, a client can send fields you never expected, and large request bodies can eat your memory. This page covers how to decode JSON in Go and how to make it strict when you need to.
json.Unmarshal
Pass a pointer to the value you want filled:
package main
import (
"encoding/json"
"fmt"
)
type Config struct {
Port int `json:"port"`
Debug bool `json:"debug"`
Origins []string `json:"origins"`
}
func main() {
input := []byte(`{"port": 8080, "debug": true, "origins": ["https://ntxm.org"]}`)
var c Config
if err := json.Unmarshal(input, &c); err != nil {
fmt.Println("bad config:", err)
return
}
fmt.Printf("%+v\n", c)
}
{Port:8080 Debug:true Origins:[https://ntxm.org]}
Forgetting the & gives json: Unmarshal(non-pointer main.Config). Field matching is case-insensitive, so "PORT" would also fill Port.
Missing and Unknown Fields Are Silent
By default the decoder is forgiving:
package main
import (
"encoding/json"
"fmt"
)
type Signup struct {
Email string `json:"email"`
Age int `json:"age"`
}
func main() {
var s Signup
err := json.Unmarshal([]byte(`{"emial": "x@example.com", "admin": true}`), &s)
fmt.Printf("%+v err=%v\n", s, err)
}
{Email: Age:0} err=<nil>
The misspelled emial and the unexpected admin are both ignored, and the missing fields stay at their zero values. There is no error. For config files and public APIs, that hides mistakes.
Rejecting Unknown Fields
json.NewDecoder has an option that Unmarshal lacks:
package main
import (
"encoding/json"
"fmt"
"strings"
)
type Signup struct {
Email string `json:"email"`
Age int `json:"age"`
}
func main() {
dec := json.NewDecoder(strings.NewReader(`{"emial": "x@example.com"}`))
dec.DisallowUnknownFields()
var s Signup
err := dec.Decode(&s)
fmt.Println("error:", err)
}
error: json: unknown field "emial"
Now typos and unexpected fields become errors you can show to the user.
Telling "Missing" from "Zero"
If age is 0, did the client send 0 or leave it out? Use a pointer field. It stays nil when the key is absent:
package main
import (
"encoding/json"
"fmt"
)
type Update struct {
Name *string `json:"name"`
Age *int `json:"age"`
}
func main() {
var u Update
json.Unmarshal([]byte(`{"age": 0}`), &u)
if u.Name == nil {
fmt.Println("name: not sent, keep the old value")
}
if u.Age != nil {
fmt.Println("age: set to", *u.Age)
}
}
name: not sent, keep the old value
age: set to 0
This pattern is common for PATCH endpoints, where only the fields the client sends should change.
Unmarshal vs NewDecoder for HTTP Bodies
The common question: which one should a handler use? json.NewDecoder reads straight from an io.Reader, so it fits r.Body without reading it into a []byte first. json.Unmarshal needs the whole input in memory, so it fits data you already have, like a file read with os.ReadFile.
Here is a handler that decodes a body safely, tested with httptest:
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
type CreateTask struct {
Title string `json:"title"`
}
func createTask(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB limit
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var t CreateTask
if err := dec.Decode(&t); err != nil {
http.Error(w, "bad JSON: "+err.Error(), http.StatusBadRequest)
return
}
if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
http.Error(w, "body must contain one JSON object", http.StatusBadRequest)
return
}
if t.Title == "" {
http.Error(w, "title is required", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, "created %q\n", t.Title)
}
func main() {
for _, body := range []string{
`{"title": "write docs"}`,
`{"title": "x", "done": true}`,
`{"title": "a"}{"title": "b"}`,
`{}`,
} {
req := httptest.NewRequest(http.MethodPost, "/tasks", strings.NewReader(body))
rec := httptest.NewRecorder()
createTask(rec, req)
fmt.Print(rec.Code, " ", rec.Body.String())
}
}
201 created "write docs"
400 bad JSON: json: unknown field "done"
400 body must contain one JSON object
400 title is required
What each step protects against:
http.MaxBytesReaderstops a client from sending a gigabyte of JSON.DisallowUnknownFieldsturns typos into errors.- The second
Decodemust returnio.EOF.Decodereads one value and stops, so without this check a body with extra data after the object is accepted. - The final check validates required fields, since JSON decoding never does that for you.
Decoding into any:
If you decode into map[string]any or any, every JSON number becomes a float64, so large IDs can lose precision. Decode into a struct with int64 fields, or call dec.UseNumber() to keep numbers as strings until you convert them.
Common Errors
json: cannot unmarshal string into Go struct field Config.port of type int
invalid character '}' looking for beginning of object key string
unexpected end of JSON input
The first means the JSON type does not match the Go field type, here "8080" in quotes for an int field. The second is malformed JSON, often a trailing comma. The third means the input was cut off or empty, which often happens when the body was already read once before decoding.
Your decoding is safe:
If your handlers limit body size, reject unknown fields, check for trailing data, and validate required fields, clients get clear 400 errors instead of half-filled structs.
Summary
Decode with json.Unmarshal when the data is already in memory, and with json.NewDecoder when reading from a stream like an HTTP body. By default unknown fields are ignored and missing ones are zero, so call DisallowUnknownFields and use pointer fields when you need to know what was sent. In handlers, wrap the body with http.MaxBytesReader and check for trailing data.