Vocabulary

August 10, 2026 · View on GitHub

Validation is exact: a hint T accepts only type(value) is T. There is no coercion, subclass acceptance or int/bool leakage.

Values that arrive in a portable tree — where a date, a time and an enum member have no carrier of their own — pass through decode first, which restores those spellings and nothing else. Everything below describes what resolve and build accept, after that step or without it.

HintShapeExample
intIntn: int
floatFloatratio: float
strStrname: str
boolBoolactive: bool
datetime.dateDateday: date
datetime.timeTimeat: time
Enum subclassEnumShaperole: Role
NoneNoneShapenormally part of X | None
list[X]Listtags: list[str]
dataclassStructpage: Page
uniontuple of shapesvalue: int | str
ambiguous uniontuple of shapesvalue: list[str] | list[int]
Literal[...]Int or Str with Choicesmode: Literal["fast", "safe"]

float does not accept int; int does not accept bool. Time requires a naive time. Enum values must be members of the exact enum class. None alone is rejected because it describes no useful field; use X | None.

There is no path type. A file input is a str marked with FileHint, which carries the contract of the file that string names — the extensions it may take, and the sizes it must fall between — while the value stays exactly str. Of that contract the core validates only the extension, which the text of the value settles by itself; the sizes are declared, written into the portable document and checked where the file actually is, at the wrapper's boundary. The core never opens a path, never asks whether it exists, and never turns it into a pathlib.Path: a str marked this way is a str that says what it names, not a file the core has been to see. See atoms.md.

There is no datetime type either, and for a different reason: a combined timestamp cannot be read one way. Whether it is aware or naive, what precision it keeps and how it is written down are policy, and a field typed datetime would leave the answers unstated. Written as a dataclass the answers are the author's, and the schema says which ones they chose:

@dataclass
class Timestamp:
    day: date
    at: time
    utc_offset_minutes: int | None = None

build(decode(...)) returns it from {"day": "2026-08-10", "at": "14:30", "utc_offset_minutes": 120}, and the offset field is where the timezone policy lives — visible to every reader of the document instead of assumed from a type name. See philosophy.md.

Lists validate their length and every indexed item. Nesting and union-valued items are supported:

from dataclasses import dataclass

@dataclass
class Created:
    id: int

@dataclass
class Deleted:
    id: int

matrix: list[list[int]]
events: list[Created | Deleted]
holes: list[int | None]

None is a valid item option: list[int | None] accepts None holes as values. A None item is data, not field optionality; list[None] alone remains rejected.

list[str | int] is not list[str] | list[int]

Python allows both, and they say different things. The core keeps them apart.

mixed: list[str | int]        # one list whose items may be either
either: list[str] | list[int] # one list of str, or one list of int

mixed routes every element by its own exact type, so it takes its value directly:

{"mixed": ["a", 1, "b", 2]}

either chooses once, for the whole list. Both options arrive as a list, so the value cannot say which one it is and the caller says it:

{"either": {"$type": "list[str]", "$value": ["a", "b"]}}
{"either": {"$type": "list[int]", "$value": [1, 2]}}

["a", 1] is valid for mixed and invalid for either under either option. The discriminator is required only where the runtime type is shared — see build.md for the wrapper and restrictions.md for the identities it names.

Dataclasses accept dictionaries as input. build recursively constructs the instance; input instances are rejected. Two or more dataclass alternatives use the inline $type discriminator described in build.md, including as list items: list[Shirt | Mug] discriminates each element, while list[Shirt] | list[Mug] wraps the whole list.

Union options retain user order. Metadata for a specific option belongs inside that option: Annotated[int, Min(0)] | str. Field atoms such as Label belong on the outer field layer.

Literal is shorthand for exact choices. Its values must all have the same type and may be only int or str; float choices use Annotated[float, Choices(values=(...))].