Input Validation & Safe HTML/JSON Handling
Every HTTP and gRPC handler that accepts user input must validate structure, size, and semantics before touching databases, shells, or HTML output.
Search across all documentation pages
Every HTTP and gRPC handler that accepts user input must validate structure, size, and semantics before touching databases, shells, or HTML output.
Go's parsers help with syntax, but they do not enforce your security policy.
Input validation in Go starts at the transport edge (body size limits, content types) and continues through typed decoding (encoding/json, json.Decoder, protobuf) and explicit rule checks (length, format, allowlists).
Safe output encoding depends on context: HTML needs html/template, JSON APIs need correct Content-Type and no embedded HTML in string fields without encoding.
Injection flaws (SQL, command, template, log) share one root cause: treating untrusted data as code or structure.
Validate early, parameterize queries, and fail with safe error messages.
Quick-reference recipe card - copy-paste ready.
type CreateUser struct {
Email string `json:"email"`
Age int `json:"age"`
}
func decodeCreateUser(r *http.Request) (CreateUser, error) {
r.Body = http.MaxBytesReader(nil, r.Body, 1<<20)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var u CreateUser
if err := dec.Decode(&u); err != nil {
return u, err
}
if !strings.Contains(u.Email, "@") || u.Age < 0 || u.Age > 150 {
return u, errors.New("invalid user")
}
return u, nil
}When to reach for this:
package main
import (
"database/sql"
"encoding/json"
"errors"
"html/template"
"net/http"
"regexp"
"strings"
_ "modernc.org/sqlite"
)
var emailRe = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
type Profile struct {
DisplayName string `json:"display_name"`
Email string `json:"email"`
}
func validateProfile(p Profile) error {
p.DisplayName = strings.TrimSpace(p.DisplayName)
if len(p.DisplayName) == 0 || len(p.DisplayName) > 64 {
return errors.New("invalid display name")
}
if !emailRe.MatchString(p.Email) {
return errors.New("invalid email")
}
return nil
}
func createProfileHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 64<<10)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var p Profile
if err := dec.Decode(&p); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if err := validateProfile(p); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
_, err := db.Exec(
"INSERT INTO profiles (display_name, email) VALUES (?, ?)",
p.DisplayName, p.Email,
)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}
}
func renderProfileHandler() http.HandlerFunc {
tmpl := template.Must(template.New("page").Parse(`<h1>{{.}}</h1>`))
return func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if len(name) > 64 {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, name)
}
}
func main() {
db, _ := sql.Open("sqlite", ":memory:")
mux := http.NewServeMux()
mux.HandleFunc("POST /profiles", createProfileHandler(db))
mux.HandleFunc("GET /view", renderProfileHandler())
http.ListenAndServe(":8080", mux)
}What this demonstrates:
DisallowUnknownFields for strict API contracts.? placeholders, not string formatting.html/template auto-escaping for query parameter display.json.Unmarshal and json.Decoder map JSON to Go types; zero values fill missing fields unless you use pointers for optional detection.go-playground/validator, custom functions) run after decode to enforce formats, ranges, and cross-field rules.DisallowUnknownFields rejects extra JSON keys - useful for public APIs; sometimes relaxed for forward-compatible internal APIs.html/template handles element text, while JavaScript or URL contexts need stricter rules or CSP.db.Query("SELECT * FROM users WHERE id = ?", id) never interpolates user strings into the query text.| Layer | Check | Example |
|---|---|---|
| Transport | Size, content type | MaxBytesReader, reject non-application/json |
| Syntax | Parse success | json.Decoder.Decode, protobuf proto.Unmarshal |
| Schema | Types, required fields | Struct tags, DisallowUnknownFields |
| Semantic | Business rules | Email format, enum allowlist, ownership |
| Authorization | Who may set this field | Strip is_admin from public create DTOs |
// Avoid mass assignment: use separate DTOs for create vs update
type PublicCreateOrder struct {
SKU string `json:"sku"`
Quantity int `json:"quantity"`
// no Status, no UserID - server sets those
}Age: -1 or Role: "admin". Always run semantic validation.is_admin, owner_id). Use input DTOs.html/template or return JSON only.fmt.Sprintf("SELECT * FROM t WHERE id = %s", id) is injectable. Use placeholders always.err.Error() from database drivers leaks schema hints. Log internally; respond with generic messages."" and omitted field both become "" on string fields. Use pointers or sql.NullString when distinction matters.| Alternative | Use When | Don't Use When |
|---|---|---|
go-playground/validator struct tags | Large DTO sets with standard formats | You need domain rules tags cannot express |
| OpenAPI + code generation | Contract-first public APIs | Small internal handlers where codegen is heavy |
| Protobuf + protovalidate | gRPC with strong schemas | Simple JSON-only REST services |
| WAF at ingress | Broad attack signatures | Substitute for app-level validation (never enough alone) |
Decoder streams and supports DisallowUnknownFields per request without allocating the full raw body.
Unmarshal is fine for small bodies after you have already capped size with MaxBytesReader.
Enable for versioned public APIs where unexpected fields signal client bugs or probing.
Internal services may allow unknown fields for forward compatibility if you document the policy.
Read with r.URL.Query().Get or mux path vars, then validate length, regex, and allowlists before use.
Never pass raw path segments into SQL or shell commands.
It auto-escapes HTML element text.
Embedding user data inside <script>, event handlers, or javascript: URLs still requires CSP and strict input rules.
Use a fixed shape like {"error":"bad request"} without stack traces or driver messages.
Log the real error with request ID server-side.
Protobuf enforces types on the wire, not business rules.
Add validation in handlers or use protovalidate / custom checks after unmarshal.
Validate URLs against an allowlist of hosts and schemes.
Block link-local, metadata IPs, and private ranges before http.Get.
Client validation improves UX.
Server validation is mandatory - clients can be bypassed entirely.
Cap size, sniff MIME type from content not extension, store outside web root, and scan if policy requires.
Never execute uploaded content.
Normalize and compare emails and usernames with a defined NFC/NFKC policy before uniqueness checks.
Document the normalization rule to avoid duplicate account edge cases.
Regex catches obvious garbage; full RFC email validation is complex.
Combine reasonable regex with confirmation flows for high-risk accounts.
Table-driven tests with boundary values: empty, max length, invalid enums, unknown JSON keys, and SQL metacharacters in string fields.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026