go 55 lines · 8 steps

Hardening JSON decoding in Go HTTP handlers

A reusable decodeJSON helper that validates input and turns cryptic decoder errors into clear, client-friendly messages.

Explained by highlit
1package handlers
2 
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io"
8 "net/http"
9 "strings"
10)
11 
12type CreateUserRequest struct {
13 Email string `json:"email"`
14 Name string `json:"name"`
15 Password string `json:"password"`
16}
17 
18func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {
19 if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
20 return fmt.Errorf("unsupported content type %q", ct)
21 }
22 
23 r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
24 
25 dec := json.NewDecoder(r.Body)
26 dec.DisallowUnknownFields()
27 
28 if err := dec.Decode(dst); err != nil {
29 var syntaxErr *json.SyntaxError
30 var typeErr *json.UnmarshalTypeError
31 var maxBytesErr *http.MaxBytesError
32 
33 switch {
34 case errors.As(err, &syntaxErr):
35 return fmt.Errorf("malformed JSON at position %d", syntaxErr.Offset)
36 case errors.As(err, &typeErr):
37 return fmt.Errorf("invalid value for field %q", typeErr.Field)
38 case errors.Is(err, io.EOF):
39 return errors.New("request body must not be empty")
40 case errors.As(err, &maxBytesErr):
41 return fmt.Errorf("request body exceeds %d bytes", maxBytesErr.Limit)
42 case strings.HasPrefix(err.Error(), "json: unknown field "):
43 field := strings.TrimPrefix(err.Error(), "json: unknown field ")
44 return fmt.Errorf("unknown field %s", field)
45 default:
46 return err
47 }
48 }
49 
50 if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
51 return errors.New("request body must contain a single JSON object")
52 }
53 
54 return nil
55}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Bounding the request body with MaxBytesReader protects the server from memory exhaustion before parsing begins.
  2. 2errors.As and errors.Is let you inspect wrapped error types and produce specific, actionable messages.
  3. 3A second Decode call is the idiomatic way to reject trailing data after the first JSON object.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Hardening JSON decoding in Go HTTP handlers — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code