Go cookbook · Beginner

Decode and validate JSON request bodies safely

Handlers accept huge bodies, unknown fields and invalid values without complaint.

Create a free account to save this page, take notes and track your progress. Sign in

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.ReadAll without 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/validator adds 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

Was this page helpful?