hen 0.20.0

Run protocol-aware API request collections from the command line or through MCP.
Documentation
---
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...`.
- `===` is not supported inside guards.

## 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.