Read JSON off an incoming request with json.Decoder, capped in size and validated before it reaches anything else.
r.Body is an io.ReadCloser, so a json.Decoder can read it straight off the wire without buffering the whole thing first.
An anonymous struct keeps the wire format next to the handler that parses it, separate from whatever domain type the data becomes later.
func createUser(w http.ResponseWriter, r *http.Request) {
var in struct {
Email string `json:"email"`
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
// use in.Email, in.Name
}Without a cap, a client can post gigabytes and the decoder will keep reading. MaxBytesReader stops at the limit and closes the connection.
Passing w as the first argument lets it tell the server not to bother reading the rest of the request.
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}Rejecting a second JSON value
Decode reads one JSON value and stops. Anything after it in the body goes unread and unreported.
More reports whether the stream has another value waiting, which turns a malformed body into a 400 instead of a silent half-read.
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&in); err != nil {
// handle error
}
// Decode stops after the first value. A body of
// {"email":"a@b.c"}{"email":"x@y.z"} passes without this.
if dec.More() {
http.Error(w, "body must hold one JSON object", http.StatusBadRequest)
return
}A body of decodes cleanly and leaves every field at its zero value. Required fields need an explicit check.
A pointer field separates "the client sent false" from "the client sent nothing", which matters on a PATCH endpoint.
if in.Email == "" {
http.Error(w, "email is required", http.StatusBadRequest)
return
}
// Or make absence detectable with a pointer field:
var in struct {
Active *bool `json:"active"`
}
if in.Active == nil {
// the client did not send the field at all
}Working program
A complete server with one endpoint, capped, strict about unknown fields, and validating the email before it answers.
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
)
type createUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
}
func createUser(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var in createUserRequest
if err := dec.Decode(&in); err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
if dec.More() {
http.Error(w, "body must hold one JSON object", http.StatusBadRequest)
return
}
if !strings.Contains(in.Email, "@") {
http.Error(w, "email is required", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"id": "u_1", "email": in.Email})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /users", createUser)
fmt.Println("listening on :8080")
http.ListenAndServe(":8080", mux)
}Run it, then post to it from another terminal.
$ curl -s -XPOST localhost:8080/users -d '{"email":"kt@example.com","name":"Keith"}'
{"email":"kt@example.com","id":"u_1"}
$ curl -s -XPOST localhost:8080/users -d '{"emial":"typo"}'
invalid JSON: json: unknown field "emial"