Go cookbook · Beginner
Decode and validate JSON request bodies safely
Handlers accept huge bodies, unknown fields and invalid values without complaint.
Approach
Limit the body size with http.MaxBytesReader, reject unknown fields and trailing data, then validate the
decoded struct and return field errors as JSON.
Solution
package api
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/mail"
"strings"
)
type SignupRequest struct {
Email string `json:"email"`
Name string `json:"name"`
Seats int `json:"seats"`
}
func (s SignupRequest) Validate() map[string]string {
errs := map[string]string{}
if _, err := mail.ParseAddress(s.Email); err != nil {
errs["email"] = "must be a valid email address"
}
if strings.TrimSpace(s.Name) == "" {
errs["name"] = "is required"
}
if s.Seats < 1 || s.Seats > 100 {
errs["seats"] = "must be between 1 and 100"
}
return errs
}
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
return fmt.Errorf("body must be at most %d bytes", maxErr.Limit)
}
return fmt.Errorf("invalid JSON: %w", err)
}
if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
return errors.New("body must contain a single JSON object")
}
return nil
}
func Signup(w http.ResponseWriter, r *http.Request) {
var req SignupRequest
if err := decodeJSON(w, r, &req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
if errs := req.Validate(); len(errs) > 0 {
writeJSON(w, http.StatusUnprocessableEntity, map[string]any{"errors": errs})
return
}
writeJSON(w, http.StatusCreated, req)
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
Pitfalls
- Reading the whole body with
io.ReadAllwithout a size limit. - Treating zero values as "not provided"; use pointers (
*int) when absence matters. - Returning raw decoder errors that expose internal type names; map them to friendly messages.
Variations
github.com/go-playground/validatoradds declarative struct tags for larger APIs.- Generate request types from an OpenAPI spec (oapi-codegen) to keep clients and server in sync.
Further reading: json.Decoder
Reference: https://pkg.go.dev/encoding/json#Decoder.DisallowUnknownFields
More Go recipes
- Benchmark code with testing.B.Loop
- Build a small, secure container image for a Go service
- Embed static files and templates in the binary
- Load and validate configuration from environment variables
- Log structured JSON with slog and redact secrets
- Process jobs concurrently with a bounded worker pool
- Profile a running service with pprof
- Rate limit requests per client