---
sidebar_position: 8
title: Schemas
description: Define scalar predicates, schema objects, aliases, and combinators for typed JSON validation.
---
## 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
```hen
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
```hen
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.
```hen
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
```hen
^ & 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.