go 102 lines · 2 tabs

Per-Field Signup Validation Errors With a Reusable Validator in Go

Shared by codesnips Aug 2026
2 tabs
package validate

import (
	"regexp"
	"unicode/utf8"
)

var EmailRX = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

type Validator struct {
	FieldErrors map[string]string `json:"field_errors"`
}

func New() *Validator {
	return &Validator{FieldErrors: map[string]string{}}
}

func (v *Validator) Valid() bool {
	return len(v.FieldErrors) == 0
}

func (v *Validator) AddError(field, message string) {
	if _, exists := v.FieldErrors[field]; !exists {
		v.FieldErrors[field] = message
	}
}

func (v *Validator) Check(ok bool, field, message string) {
	if !ok {
		v.AddError(field, message)
	}
}

func NotBlank(value string) bool {
	return value != ""
}

func MinChars(value string, n int) bool {
	return utf8.RuneCountInString(value) >= n
}

func MaxChars(value string, n int) bool {
	return utf8.RuneCountInString(value) <= n
}

func Matches(value string, rx *regexp.Regexp) bool {
	return rx.MatchString(value)
}
2 files · go Explain with highlit

This snippet shows how a Go HTTP service validates a signup form and returns structured, per-field errors instead of a single opaque message. The goal is a JSON shape a frontend can bind directly to input fields, so each rule failure is attached to the name of the field it concerns.

In validator.go, Validator is a tiny accumulator around a map[string]string keyed by field name. Its Check method records a message only when a condition is false and only if that field has no error yet, which means the first failing rule per field wins and messages don't get clobbered. Keeping one message per field is a deliberate trade-off: it keeps the UI simple at the cost of not reporting every possible problem at once. Valid reports whether any errors accumulated, and helpers like NotBlank, MinChars, and Matches are pure predicates so the rules read declaratively at the call site. EmailRX is compiled once at package load with regexp.MustCompile to avoid recompiling on every request.

In handler.go, SignupHandler decodes the request body into a SignupForm. json.NewDecoder with DisallowUnknownFields rejects unexpected keys early, guarding against typos and stale clients. The handler then constructs a Validator and expresses every business rule through v.Check: presence, length bounds, email format via Matches(EmailRX, ...), and a cross-field rule confirming Password equals PasswordConfirm. This separation matters — the handler owns the policy (which rules apply) while Validator owns the mechanics of collecting failures.

When v.Valid() is false, writeJSON returns HTTP 422 Unprocessable Entity with the FieldErrors map, the conventional status for a well-formed request that fails semantic validation. A generic "" key carries non-field errors such as a malformed JSON body.

The pattern scales well: new rules are one-liners, the response contract stays stable, and the validator has no dependency on net/http, so the same logic can back a CLI or a gRPC layer. A pitfall to watch is trusting client-side checks; server-side validation here is authoritative. It also normalizes input with strings.TrimSpace before checking, so whitespace-only fields are correctly treated as blank.


Related snips

Share this code

Here's the card — post it anywhere.

Per-Field Signup Validation Errors With a Reusable Validator in Go — share card
Link copied