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.MaxBytesReader stops a client from sending a gigabyte of JSON.
  • DisallowUnknownFields turns typos into errors.
  • The second Decode must return io.EOF. Decode reads 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.