Skip to content
Zever
Home

Validation

Validate input with .zen rules and handler checks, and fail with precise, field-named errors.

2 min readEdit on GitHub

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.

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.

handlers.go
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})
}
Terminal window
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:).
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.

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.

  • 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).
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).