- Docs
- HTTP APIs
- Validation
Validation
Validate input with .zen rules and handler checks, and fail with precise, field-named errors.
Validation is @validate rules in .zen (shape/format) plus Go checks in handlers (transport/business). Both fail with field-named errors, never silent coercion; there is no config block or accessor.
Validate a request
Section titled “Validate a request”Declare shape rules in the schema (Zen syntax):
rpc CreateUser(email: string @validate(format: "email")) -> User { http: POST "/v1/users" errors: { already_exists }}
// on an entity field:title: string @validate(min_len: 1, max_len: 200)The resolver checks each expression at compile time; zever check reports bad rules with positions, and @validate reaches generated code and OpenAPI.
Then parse, validate, and only then call db, queue, or payment; failed validation must never open a transaction or cause a side effect.
func createUser(w http.ResponseWriter, r *http.Request) { var in struct { Email string `json:"email"` Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&in); err != nil { writeError(w, http.StatusBadRequest, "invalid body") return } if !strings.Contains(in.Email, "@") { writeError(w, http.StatusUnprocessableEntity, "email: must be a valid address") return } if strings.TrimSpace(in.Name) == "" { writeError(w, http.StatusUnprocessableEntity, "name: required") return } writeJSON(w, http.StatusCreated, map[string]string{"email": in.Email})}curl -s -X POST localhost:8080/v1/users -d '{"email":"a@example.com","name":"Ada"}'{"email":"a@example.com"}Invalid input answers 422. writeJSON/writeError come from Requests and responses.
| Status | When |
|---|---|
400 |
Malformed input. |
422 |
Well-formed but invalid. |
409 |
already_exists (RPC errors:). |
Split rules between schema and handler
Section titled “Split rules between schema and handler”| Rule kind | Owner | Example |
|---|---|---|
| Shape/format | .zen @validate |
email, title length |
| Transport | Handler | JSON, headers |
| Business state | Handler/store | Email not registered (409) |
| Config sanity | Startup Validate |
Options, DSN |
Bookings keeps format rules on entities (User.email, Space.title) and flow rules in internal/api; a core/flag gate returns 404, ErrConflict maps to 409. OpenAPI covers only the schema.
Validate config at startup
Section titled “Validate config at startup”config.Load(path) decodes strictly, so unknown file fields fail fast. An unparsable env value returns InvalidOptionsError (e.g. DB_MAXCONNS=abc) naming the service and field, never the secret; unknown env vars are ignored.
Production checklist
Section titled “Production checklist”- Name the field and rule, never the rejected secret.
- Validate before I/O: no deadline, nothing to drain on close.
- RPC error-set changes are API changes (
breaking).
Troubleshooting
Section titled “Troubleshooting”zever check rejects a rule the handler accepts
The schema expression has a typo or unsupported constraint; the resolver is the authority.
Clients get 400 for business errors
Return 400 only for unparsable bodies, 422 for well-formed invalid values.
409 never fires on duplicates
The RPC lacks errors: { already_exists }, or the handler misses the conflict.
Boot fails on a config typo
Strict decoding rejects unknown file fields. Use the snake_case key (max_conns, not maxconns).