Decoding JSON Request Bodies
Turn a JSON request body into a Go struct with json.Decoder, reject unknown fields, and cap how many bytes you read.
dec := json.NewDecoder(r.Body); dec.DisallowUnknownFields(); err := dec.Decode(&v)A request body is the block of bytes a client sends along with a request. When the client is a program rather than a browser form, that body is usually JSON: text that describes an object as names paired with values. Your handler has to turn it into a Go value before it can use it.
json.NewDecoder takes anything you can read bytes from. r.Body is one of those things. Decode then reads one JSON value and fills in the struct you point it at.
Three guards, in order
Cap the size first. http.MaxBytesReader wraps the body and stops it after the number of bytes you pass. Without it a client can send a gigabyte and your process will try to hold all of it. Then call DisallowUnknownFields on the decoder. A field the struct does not have becomes an error instead of being thrown away in silence, which catches typos in client code. Then check the error from Decode and answer with a 400, rather than letting it surface as a 500.
Shorthand and longhand
json.Unmarshal(data, &v) is the other way in. It needs the whole body in memory first, so the long form is io.ReadAll and then Unmarshal. json.NewDecoder(r.Body).Decode(&v) is the same work in one step, and it reads as it goes. Both produce the same value.
Version note
In Go 1.27 encoding/json is backed by the newer encoding/json/v2 implementation. The behaviour is preserved; the exact wording of error messages can differ from earlier versions.
Example
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
type signup struct {
Email string `json:"email"`
Age int `json:"age"`
}
func handle(w http.ResponseWriter, r *http.Request) {
// Stop reading after 4 KB, whatever the client claims to be sending.
r.Body = http.MaxBytesReader(w, r.Body, 4<<10)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // a field signup lacks is now an error
var in signup
if err := dec.Decode(&in); err != nil {
http.Error(w, "bad body: "+err.Error(), http.StatusBadRequest)
return
}
fmt.Fprintf(w, "email=%s age=%d\n", in.Email, in.Age)
}
func main() {
// Stand in for a real client. curl -d '{"email":"[email protected]","age":30}' /signup
body := strings.NewReader(`{"email":"[email protected]","age":30}`)
req := httptest.NewRequest(http.MethodPost, "/signup", body)
rec := httptest.NewRecorder()
handle(rec, req)
fmt.Print(rec.Body.String())
}
// Output:
// [email protected] age=30When to use it
- Accept a POST /orders body from a mobile app and reject it with 400 when a field name is misspelled.
- Cap request bodies at 1 MB on a public endpoint so one client cannot exhaust server memory.
- Read a webhook payload from a payment provider into a struct before verifying its contents.
More examples
The longhand: ReadAll then Unmarshal
json.Unmarshal takes a byte slice, so you have to collect the whole body yourself first. Reach for this form when you need the raw bytes for something else as well, such as checking a signature header over the exact body that was sent. Otherwise prefer the decoder, which does not need the whole body in memory at once.
// Same result as json.NewDecoder(r.Body).Decode(&in), written out in full.
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 4<<10))
if err != nil {
http.Error(w, "could not read body", http.StatusBadRequest)
return
}
var in signup
if err := json.Unmarshal(data, &in); err != nil {
http.Error(w, "bad body", http.StatusBadRequest)
return
}Empty bodies and trailing junk
An empty body gives io.EOF, which deserves its own message because the fix is different. The second Decode is the only way to notice a body like {"a":1}garbage, which the first Decode accepts. Add both checks to any endpoint whose clients you do not control.
var in signup
switch err := dec.Decode(&in); {
case errors.Is(err, io.EOF):
// The body was empty. That is a client mistake, not a server one.
http.Error(w, "a JSON body is required", http.StatusBadRequest)
return
case err != nil:
http.Error(w, "malformed JSON", http.StatusBadRequest)
return
}
// Decode stopped after one value. Insist there is nothing after it.
if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
http.Error(w, "body must contain a single JSON object", http.StatusBadRequest)
return
}Turning decode errors into messages a client can act on
errors.As checks whether an error is, or wraps, a particular type and copies it into your variable. json.SyntaxError carries the byte offset, json.UnmarshalTypeError carries the field path, and http.MaxBytesError carries the limit that was passed to MaxBytesReader. Use this in a shared helper so every endpoint gives the same quality of message.
var (
syn *json.SyntaxError
mism *json.UnmarshalTypeError
tooBig *http.MaxBytesError
)
switch {
case errors.As(err, &syn):
msg := fmt.Sprintf("malformed JSON at byte %d", syn.Offset)
http.Error(w, msg, http.StatusBadRequest)
case errors.As(err, &mism):
// Field is the path to the offending value, e.g. "age".
msg := fmt.Sprintf("field %q has the wrong type", mism.Field)
http.Error(w, msg, http.StatusBadRequest)
case errors.As(err, &tooBig):
msg := fmt.Sprintf("body must be under %d bytes", tooBig.Limit)
http.Error(w, msg, http.StatusRequestEntityTooLarge)
default:
http.Error(w, "bad request body", http.StatusBadRequest)
}
Discussion