Skip to main content

Schemas

Where Declarations Live

Schema declarations live in the collection preamble before the first ---.

  • Imported fragments brought in with << can contribute reusable scalar and schema declarations.
  • 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 string or number.
  • The base target may also be a built-in scalar target such as UUID or DATE_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 as enum(...).
  • enum(value1, value2, ...) where each value is a quoted string, integer, decimal number, true, false, or null
  • format(NAME) where NAME is one of UUID, EMAIL, NUMBER, DATE, DATE_TIME, TIME, or URI
  • len(min..max) for string length checks with inclusive integer bounds; either bound may be omitted as in len(3..) or len(..24), but not both
  • pattern(/regex/) for string checks against a slash-delimited regular expression
  • range(min..max) for numeric checks with inclusive integer or decimal bounds; either bound may be omitted as in range(0..) or range(..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?: Type marks a field as optional.
  • Type? marks the value itself as nullable.
  • schema Name = Target aliases 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(...), and anyOf(...) require at least two targets.
  • discriminator(...) requires at least one branch.
  • OpenAPI imports commonly emit const(...) tag scalars together with oneOf(...), anyOf(...), allOf(...), or discriminator(...) declarations.

Primitive And Built-In Targets

Primitive types available inside declarations are:

  • string
  • integer
  • number
  • boolean
  • null

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 UUID or NUMBER.

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.