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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Bounding the request body with MaxBytesReader protects the server from memory exhaustion before parsing begins.
- 2errors.As and errors.Is let you inspect wrapped error types and produce specific, actionable messages.
- 3A second Decode call is the idiomatic way to reject trailing data after the first JSON object.
Related explainers
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
8 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
go
package auth import ( "net/http"
Setting and reading secure session cookies in Go
cookies
session-management
security
Intermediate
6 steps
rust
use std::cmp::Ordering; use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq)]
Parsing and ordering semantic versions in Rust
parsing
trait-implementation
ordering
Intermediate
8 steps
javascript
'use client'; import { useEffect } from 'react'; import * as Sentry from '@sentry/nextjs';
How a Next.js error boundary recovers
error-boundary
error-handling
observability
Intermediate
8 steps
ruby
class Registration < ApplicationRecord belongs_to :event validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
Validating registrations in Rails
validations
i18n
error-handling
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/hardening-json-decoding-in-go-http-handlers-explained-go-2723/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.