Validation, migrate & sqlc
Three focused libraries cover common data-layer edges in Go services: go-playground/validator for struct tags, golang-migrate for schema versions, and sqlc for type-safe SQL codegen.
Search across all documentation pages
Three focused libraries cover common data-layer edges in Go services: go-playground/validator for struct tags, golang-migrate for schema versions, and sqlc for type-safe SQL codegen.
They complement database/sql without imposing a full ORM.
Quick-reference recipe card - copy-paste ready.
import "github.com/go-playground/validator/v10"
var validate = validator.New()
type CreateUser struct {
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=18,lte=120"`
}
func check(in CreateUser) error {
return validate.Struct(in)
}migrate -path ./migrations -database "${DATABASE_URL}" up
sqlc generateWhen to reach for this:
QueryRow callspackage api
import (
"context"
"encoding/json"
"net/http"
"github.com/go-playground/validator/v10"
)
var validate = validator.New()
type createUserRequest struct {
Email string `json:"email" validate:"required,email"`
Name string `json:"name" validate:"required,min=1,max=128"`
}
func createUser(w http.ResponseWriter, r *http.Request) {
var in createUserRequest
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if err := validate.Struct(in); err != nil {
http.Error(w, formatValidation(err), http.StatusBadRequest)
return
}
// call service with validated DTO
w.WriteHeader(http.StatusCreated)
}
func formatValidation(err error) string {
// map validator.ValidationErrors to field-scoped JSON in production
return err.Error()
}-- migrations/000001_create_users.up.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL
);# sqlc.yaml
version: "2"
sql:
- engine: "postgresql"
queries: "queries/"
schema: "migrations/"
gen:
go:
package: "db"
out: "internal/db"What this demonstrates:
validator.Validate instance.up/down SQL in order.sqlc generate when queries or schema change.| Tag | Meaning |
|---|---|
required | Non-zero value |
email | RFC-ish email format |
uuid | UUID string |
gte=0 / lte=100 | Numeric bounds |
oneof=red green | Enum strings |
| Practice | Why |
|---|---|
| One change per migration file | Easier rollback and review |
| Expand-contract for zero downtime | Add column nullable before backfill |
| Run in CI against ephemeral DB | Catch SQL syntax early |
| Never from request handlers | Avoid race with deploy jobs |
queries/users.sql with -- name: GetUser :one annotations.internal/db package or regenerate in CI.// Gin/Echo binding can wrap validator - still keep DTOs at the edge
// database/sql remains the runtime driver; sqlc does not replace pgx pool tuningvalidator.ValidationErrors to problem+json for API clients.sqlc.yaml.validator.New() per package or test helper with t.Parallel care.up only; broken down hides until emergency. Fix: test down 1 in CI on disposable DBs.| Alternative | Use When | Don't Use When |
|---|---|---|
| Hand-rolled validation | Few fields, no tags | Large OpenAPI-generated DTOs |
| goose migrations | Team prefers Go migration files | You already standardized on migrate CLI |
| GORM / ent ORM | Rapid CRUD prototypes | You need tuned SQL and explicit queries |
| pgx without sqlc | Tiny query count | Dozens of queries drift from schema |
Wire framework validators to the same validator.Validate instance; keep DTO structs shared across frameworks.
Both work; pick one per org and document CLI in deploy runbooks - databases section compares them in depth.
It replaces stringly queries and some scanning boilerplate, not migrations, connection pooling, or transaction policy.
Use expand-contract migrations: add nullable columns, dual-write, backfill, then enforce NOT NULL in a later migration.
sqlc supports pgx driver options in generated code - configure in sqlc.yaml per project needs.
See databases section for embed patterns; migrate CLI still needs a filesystem or io.Source at runtime.
Yes - protobuf checks wire format, not business rules; reuse validator or hand checks on converted DTOs.
Table-driven tests on validate.Struct with good and bad DTO literals; no database required.
Register custom translations on validator or map tags to locale keys in your HTTP error mapper.
They are input hygiene, not authorization - always enforce authz in services after validation.
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 linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026