Validators
Validators are decorators that constrain field values. They live in the DSL and run at request time as plain Go code - no reflection, no struct tags, no runtime cost beyond the comparisons themselves.
At a glance
type CreateUserReq {
name string @length(1, 80)
email string @format(email)
age int? @gte(0) @lte(150)
}15+ built-in validators cover strings (length, pattern, format), numbers (min, max, range), arrays (minItems, maxItems, uniqueItems), and cross-field rules (@requiresOneOf, @mutuallyExclusive).
The handler calls req.Validate() after JSON decode and before your business logic. If validation fails, the handler returns a 400 with a structured message. Your service code never runs with bad input.
The rest of this page covers each validator family with examples.
How it works
You write:
type CreateUserReq {
name string @length(1, 80)
email string @format(email)
age int? @gte(0) @lte(150)
}craftgo generates:
func (v *CreateUserReq) Validate() error {
if l := len(v.Name); l < 1 || l > 80 {
return fmt.Errorf("name: length out of range [1, 80]")
}
if !_pattern0.MatchString(v.Email) {
return fmt.Errorf("email: does not match pattern")
}
if v.Age != nil {
if *v.Age < 0 {
return fmt.Errorf("age: below minimum 0")
}
if *v.Age > 150 {
return fmt.Errorf("age: above maximum 150")
}
}
return nil
}Plain Go. No reflection. No struct tag parsing. The handler calls req.Validate() after JSON decode and before your business logic.
Required-by-default
A non-optional field (no ?) is required, but craftgo only emits an explicit presence check when the field has a meaningful empty value the JSON decoder accepts (e.g. any, an enum). For a plain string, the decoder already rejects a literal null, and an empty "" is a legal value unless you add @length / @minLength. That's why name above shows only its @length check, not a separate "required" line. The @format(email) regex is compiled once into a package-level _pattern0 var, not per call.
Built-in validators
Required-by-default: every field gets an automatic presence check unless the type carries
?. No@requireddecorator — use?to opt-out,@nullableto keep the field mandatory while allowing JSONnull,@default(...)to pre-fill when absent (auto-marks optional on save).
The tables below cover validators with the examples that matter for validation. For the one-grid lookup of every decorator (including non-validator ones) and its legal levels, see the Decorator Registry.
Strings
| Decorator | Effect |
|---|---|
@length(min, max) | Character count in [min, max] |
@minLength(n) | At least n characters |
@maxLength(n) | At most n characters |
@pattern("regex") | Must match regexp |
@format(name) | Built-in format check (see below) |
Built-in formats: email, url, uri, uuid, datetime (RFC 3339), date, time, phone, hostname, ipv4, ipv6, cidr, mac, creditcard, base64, base64url, hexcolor, json. The RFC-compliant subset (email, ipv4/ipv6, cidr, mac, datetime/date/time, base64, json) delegates to the Go standard library; the rest use regex.
type Profile {
email string @format(email)
website string @format(uri)
avatar string @pattern("^https://.*\\.(png|jpg)$")
}Numbers
| Decorator | Effect |
|---|---|
@gte(n) | Value >= n (inclusive) |
@lte(n) | Value <= n (inclusive) |
@gt(n) | Value > n (strict) |
@lt(n) | Value < n (strict) |
@range(min, max) | Both bounds, inclusive |
@positive | > 0 (alias for @gt(0)) |
@negative | < 0 (alias for @lt(0)) |
@multipleOf(n) | Divisible by n (integers only) |
type Order {
quantity int @positive @lte(1000)
price int @gte(0) @multipleOf(2)
rating float @range(0.0, 5.0)
}Arrays
| Decorator | Effect |
|---|---|
@minItems(n) | At least n elements |
@maxItems(n) | At most n elements |
@uniqueItems | All elements distinct |
type Post {
tags string[] @minItems(1) @maxItems(10) @uniqueItems
}Cross-field
| Decorator | Effect |
|---|---|
@requiresOneOf(a, b, c) | At least one of named fields must be set |
@mutuallyExclusive(a, b) | At most one of named fields can be set |
@requiresOneOf(email, phone)
@mutuallyExclusive(personal, business)
type Contact {
email string?
phone string?
personal bool
business bool
}These attach to the type, not a field. The validator surfaces a single message.
Optional fields
A T? field becomes *T in Go. Validators only fire when the pointer is non-nil:
type UpdateUser {
name string? @length(1, 80)
}Sending {"name": null} or omitting name skips the length check. Sending {"name": "alice"} runs it.
Scalars carry validators
Scalars let you bake validators into a named primitive:
scalar Email string @format(email) @maxLength(254)
type User { email Email }User.Validate() runs the format and length checks on email because the scalar's validators inherit. No need to repeat them on every field.
File uploads
Multipart fields use @maxSize and @mimeTypes:
type AvatarReq {
userId string @path
file file @form @maxSize(2MB) @mimeTypes(["image/png", "image/jpeg"])
}Default values
@default provides a fallback when the client omits a field. The handler pre-fills the request struct before JSON decode, so omitted fields keep the default; explicit values overwrite.
type ListUsersReq {
page int @default(1)
pageSize int @default(20) @gte(1) @lte(100)
sort string? @default("created_at")
}@default works on primitives, scalars, enums, and arrays of those. The field must be optional (?) for the default to fire — the formatter auto-adds ? on save when missing, and the semantic analyzer warns until you save.
Error messages
Generated messages follow the shape:
<field>: <reason>Validate() is fail-fast — it returns the first violation it hits and stops, so a request with several problems surfaces one message at a time:
name: length out of range [1, 80]Reason strings are fixed per validator: length out of range [lo, hi], does not match pattern, below minimum N, above maximum N, out of range [lo, hi] (for @range), must be a multiple of N, maxItems N, items must be unique, etc.
Customize the response by overriding server.SetDefaultValidationFailed:
server.SetDefaultValidationFailed(func(w http.ResponseWriter, r *http.Request, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
})Validators on response types
Validate() exists on every type, including responses. craftgo does not call it on response types automatically - that would charge runtime cost for trusted server output. If you want belt-and-braces, call it yourself before encoding.
Adding a custom validator
The DSL ships a closed set of validators. To add a project-specific check, validate inside your business logic:
func (s *Service) CreateUser(ctx context.Context, req *types.CreateUserReq) (*types.User, error) {
if err := req.Validate(); err != nil {
return nil, err
}
if !s.svcCtx.AllowList.Contains(req.Email) {
return nil, types.NewBadRequestErr(types.BadRequestBody{
Message: "email domain not allowed",
})
}
// ...
}For checks that should live closer to the schema (uniqueness, foreign keys, business rules), the typed error pattern keeps the response shape clean.