Schemas
Where Declarations Live
Schema declarations live in the collection preamble before the first ---.
- Imported fragments brought in with
<<can contribute reusablescalarandschemadeclarations. - Scalar and schema references are resolved after the full preprocessed collection is loaded, so forward references are valid across the preamble and imported fragments.
Scalar Declarations
scalar HANDLE = string & len(3..24) & pattern(/^[a-z][a-z0-9_]*$/)
scalar CardKind = const("card")
scalar Contact = enum("email", "sms")
scalar CreatedAt = DATE_TIME
Scalar declarations start from one optional base target, then refine it with predicates.
- The base target may be a primitive type such as
stringornumber. - The base target may also be a built-in scalar target such as
UUIDorDATE_TIME. - The base target may also be another named scalar declaration.
- Predicates are chained with
&.
Supported predicate forms are:
const(value)for an exact literal match. This is especially useful for discriminator tags and matches the same literal forms asenum(...).enum(value1, value2, ...)where each value is a quoted string, integer, decimal number,true,false, ornullformat(NAME)whereNAMEis one ofUUID,EMAIL,NUMBER,DATE,DATE_TIME,TIME, orURIlen(min..max)for string length checks with inclusive integer bounds; either bound may be omitted as inlen(3..)orlen(..24), but not bothpattern(/regex/)for string checks against a slash-delimited regular expressionrange(min..max)for numeric checks with inclusive integer or decimal bounds; either bound may be omitted as inrange(0..)orrange(..10.5), but not both
Schema Declarations
scalar Food = enum("pizza", "taco", "salad")
schema Address {
city: string
postalCode: string
}
schema User {
id: UUID
email: EMAIL
birthday?: DATE?
favoriteFood?: Food
address: Address
}
schema ContactAddress = EMAIL
schema Users = User[]
schema MaybeBirthday = DATE?
- Object fields are open by default in v1, so extra fields are ignored during validation.
- Schema fields can reference primitive types, built-in scalar targets, named scalar declarations, and other named schemas.
field?: Typemarks a field as optional.Type?marks the value itself as nullable.schema Name = Targetaliases another target.schema Name = Target[]defines a root-array schema.
Schema Combinators
Hen also supports schema assignment expressions for unions, intersections, exclusions, and tagged dispatch.
scalar CardKind = const("card")
scalar BankKind = const("bank")
schema CardCheckout {
method: CardKind
cardLast4: string
}
schema BankCheckout {
method: BankKind
accountId: string
}
schema Checkout = discriminator(method,
"card": CardCheckout,
"bank": BankCheckout
)
schema PaymentMethod = oneOf(CardCheckout, BankCheckout)
schema ContactMethod = anyOf(EMAIL, URI)
schema CombinedCheckout = allOf(Checkout, BankCheckout)
schema NonCardCheckout = not(CardCheckout)
allOf(A, B, ...)requires all listed targets to validate.oneOf(A, B, ...)requires exactly one listed target to validate.anyOf(A, B, ...)requires at least one listed target to validate.not(Target)requires the target not to validate.discriminator(field, "tag": Target, ...)selects a branch by a field value, then validates the matching branch target.allOf(...),oneOf(...), andanyOf(...)require at least two targets.discriminator(...)requires at least one branch.- OpenAPI imports commonly emit
const(...)tag scalars together withoneOf(...),anyOf(...),allOf(...), ordiscriminator(...)declarations.
Primitive And Built-In Targets
Primitive types available inside declarations are:
stringintegernumberbooleannull
Built-in scalar targets include:
-
UUID -
EMAIL -
NUMBER -
DATE -
DATE_TIME -
TIME -
URI -
Built-in scalar targets are always available and do not need local declarations.
-
Built-in scalar target names are reserved and cannot be redefined.
-
Use lowercase primitive types inside declarations, and use built-in uppercase targets when you want a reusable named validation target such as
UUIDorNUMBER.
Assertion Use
^ & body.id === UUID
^ & body.total === NUMBER
^ & body === User
^ &[Create Checkout].body === Checkout
^ & json(body.encodedPayload).user === User
Use === when you want typed validation instead of plain string comparison.
- The right-hand side of
===must be a built-in scalar target, named scalar declaration, or named schema declaration. - The left-hand side must be a typed JSON operand such as
body...,json(...), or a dependency body read like&[Request Name].body.... ===may also be used inside guards when the left-hand side resolves to typed JSON.- In guards, a schema mismatch evaluates to
false, while invalid targets and untyped left-hand operands still surface a real error.
Practical Notes
- Keep declarations in the preamble so requests can reuse them across captures and assertions.
- Use named scalars to centralize low-level checks such as tags, IDs, and constrained strings.
- Use named schemas to keep assertions short even when response bodies are deeply nested.
- Use combinators when plain object schemas are not enough, especially for OpenAPI-generated union shapes.