Skip to content

API Reference

AirField: UI presentation vocabulary for Pydantic models.

annotated-types is for validation. airfield is for presentation. Define once on the model, render anywhere.

Autofocus dataclass

Bases: BasePresentation

This field should receive input focus when the UI context loads.

In a form: sets the autofocus attribute on the input. In a CLI: this field is prompted first. In a TUI: this widget receives initial focus.

Only one field per form/view should have this.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Autofocus(BasePresentation):
    """This field should receive input focus when the UI context loads.

    In a form: sets the autofocus attribute on the input.
    In a CLI: this field is prompted first.
    In a TUI: this widget receives initial focus.

    Only one field per form/view should have this.
    """

BasePresentation

Base class for all presentation metadata.

Consumers can do isinstance(m, BasePresentation) while traversing field annotations to find all airfield metadata.

Source code in src/airfield/types.py
class BasePresentation:
    """Base class for all presentation metadata.

    Consumers can do ``isinstance(m, BasePresentation)`` while
    traversing field annotations to find all airfield metadata.
    """

    __slots__ = ()

Choices dataclass

Bases: BasePresentation

Constrains the field to a set of labeled options.

In a form: rendered as a <select>, radio group, or combobox. In a CLI: a numbered menu or autocomplete. In API docs: an enumerated parameter.

Each option is (value, display_label).

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Choices(BasePresentation):
    """Constrains the field to a set of labeled options.

    In a form: rendered as a ``<select>``, radio group, or combobox.
    In a CLI: a numbered menu or autocomplete.
    In API docs: an enumerated parameter.

    Each option is ``(value, display_label)``.
    """

    options: tuple[tuple[Any, str], ...]

    def __init__(self, *options: tuple[Any, str]) -> None:
        object.__setattr__(self, "options", options)

ColumnAlign dataclass

Bases: BasePresentation

How to align the field's value in a table column.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class ColumnAlign(BasePresentation):
    """How to align the field's value in a table column."""

    align: Literal["left", "center", "right"]

ColumnWidth dataclass

Bases: BasePresentation

Preferred width for the field in a table column.

Expressed as a relative weight. A field with weight=2 gets roughly twice the space of a field with weight=1.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class ColumnWidth(BasePresentation):
    """Preferred width for the field in a table column.

    Expressed as a relative weight. A field with weight=2 gets
    roughly twice the space of a field with weight=1.
    """

    weight: float = 1.0
    min_chars: int | None = None
    max_chars: int | None = None

Compact dataclass

Bases: BasePresentation

How to represent this field in space-constrained contexts.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Compact(BasePresentation):
    """How to represent this field in space-constrained contexts."""

    format: str | None = None
    max_length: int | None = None

CsrfToken dataclass

Bases: BasePresentation

Marks this field as a CSRF protection token.

Form renderers should render this as a hidden input with a signed value. Form validators should verify the signature before processing other fields. The field should not appear in user-facing form layouts.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class CsrfToken(BasePresentation):
    """Marks this field as a CSRF protection token.

    Form renderers should render this as a hidden input with a
    signed value. Form validators should verify the signature
    before processing other fields. The field should not appear
    in user-facing form layouts.
    """

DisplayFormat dataclass

Bases: BasePresentation

How to format the field's value for display (not input).

Common patterns

"percent" 0.42 -> "42%" "currency" 1234.5 -> "$1,234.50" "bytes" 1048576 -> "1 MB" "relative_time" datetime -> "3 hours ago" "%Y-%m-%d" datetime -> "2026-03-18"

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class DisplayFormat(BasePresentation):
    """How to format the field's value for display (not input).

    Common patterns:
        ``"percent"``       0.42 -> "42%"
        ``"currency"``      1234.5 -> "$1,234.50"
        ``"bytes"``         1048576 -> "1 MB"
        ``"relative_time"`` datetime -> "3 hours ago"
        ``"%Y-%m-%d"``      datetime -> "2026-03-18"
    """

    pattern: str
    locale: str | None = None

Filterable dataclass

Bases: BasePresentation

Whether and how this field should appear in search/filter UI.

In a table: adds a filter control to the column. In an admin panel: adds to the filter sidebar. In a CLI list command: adds a --field=value filter flag.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Filterable(BasePresentation):
    """Whether and how this field should appear in search/filter UI.

    In a table: adds a filter control to the column.
    In an admin panel: adds to the filter sidebar.
    In a CLI list command: adds a ``--field=value`` filter flag.
    """

    kind: Literal["exact", "contains", "range", "multi_select"] = "exact"

Grouped dataclass

Bases: BasePresentation

Assigns the field to a named group for layout purposes.

In a form: fields in the same group appear in the same fieldset. In a table: groups can become column groups. In a detail view: groups become sections.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Grouped(BasePresentation):
    """Assigns the field to a named group for layout purposes.

    In a form: fields in the same group appear in the same fieldset.
    In a table: groups can become column groups.
    In a detail view: groups become sections.
    """

    name: str
    order: int = 0

HelpText dataclass

Bases: BasePresentation

Explanatory text that supplements the label.

In a form: text below the input. In a CLI: shown when the user asks for help on a field. In API docs: the parameter description. In a table: tooltip on the column header.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class HelpText(BasePresentation):
    """Explanatory text that supplements the label.

    In a form: text below the input.
    In a CLI: shown when the user asks for help on a field.
    In API docs: the parameter description.
    In a table: tooltip on the column header.
    """

    text: str

Hidden dataclass

Bases: BasePresentation

Field should not be shown in the specified contexts.

With no arguments, hidden everywhere. Standard context names: "form", "table", "detail", "api", "cli", "export"

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Hidden(BasePresentation):
    """Field should not be shown in the specified contexts.

    With no arguments, hidden everywhere. Standard context names:
    ``"form"``, ``"table"``, ``"detail"``, ``"api"``, ``"cli"``, ``"export"``
    """

    contexts: tuple[str, ...] = ()

    def __init__(self, *contexts: str) -> None:
        object.__setattr__(self, "contexts", contexts)

    def in_context(self, context: str) -> bool:
        """True if the field is hidden in the given context."""
        return not self.contexts or context in self.contexts

in_context(context)

True if the field is hidden in the given context.

Source code in src/airfield/types.py
def in_context(self, context: str) -> bool:
    """True if the field is hidden in the given context."""
    return not self.contexts or context in self.contexts

Label dataclass

Bases: BasePresentation

Human-readable name for the field.

Used everywhere a field needs a display name: form labels, table headers, CLI prompts, chart axis labels, API docs.

Without this, consumers fall back to the field name with underscores replaced by spaces and title-cased.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Label(BasePresentation):
    """Human-readable name for the field.

    Used everywhere a field needs a display name: form labels,
    table headers, CLI prompts, chart axis labels, API docs.

    Without this, consumers fall back to the field name with
    underscores replaced by spaces and title-cased.
    """

    text: str

Placeholder dataclass

Bases: BasePresentation

Example or hint text shown when the field is empty.

In a form: the placeholder attribute. In a CLI: shown in parentheses after the prompt. In API docs: the example value.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Placeholder(BasePresentation):
    """Example or hint text shown when the field is empty.

    In a form: the placeholder attribute.
    In a CLI: shown in parentheses after the prompt.
    In API docs: the example value.
    """

    text: str

PrimaryKey dataclass

Bases: BasePresentation

Marks this field as the primary identity for the record.

Affects presentation across contexts: typically hidden in create forms, read-only in edit forms, displayed as a link in tables, used as the record identifier in detail views and URLs.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class PrimaryKey(BasePresentation):
    """Marks this field as the primary identity for the record.

    Affects presentation across contexts: typically hidden in create
    forms, read-only in edit forms, displayed as a link in tables,
    used as the record identifier in detail views and URLs.
    """

Priority dataclass

Bases: BasePresentation

How important this field is relative to siblings.

Higher priority fields are shown first, or shown when space is limited while lower-priority fields are hidden.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Priority(BasePresentation):
    """How important this field is relative to siblings.

    Higher priority fields are shown first, or shown when space
    is limited while lower-priority fields are hidden.
    """

    level: int

ReadOnly dataclass

Bases: BasePresentation

Field should be displayed but not editable.

With no arguments, read-only everywhere.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class ReadOnly(BasePresentation):
    """Field should be displayed but not editable.

    With no arguments, read-only everywhere.
    """

    contexts: tuple[str, ...] = ()

    def __init__(self, *contexts: str) -> None:
        object.__setattr__(self, "contexts", contexts)

    def in_context(self, context: str) -> bool:
        """True if the field is read-only in the given context."""
        return not self.contexts or context in self.contexts

in_context(context)

True if the field is read-only in the given context.

Source code in src/airfield/types.py
def in_context(self, context: str) -> bool:
    """True if the field is read-only in the given context."""
    return not self.contexts or context in self.contexts

Sortable dataclass

Bases: BasePresentation

Whether this field should be sortable in list/table contexts.

When default is True, this field is the initial sort key.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Sortable(BasePresentation):
    """Whether this field should be sortable in list/table contexts.

    When ``default`` is True, this field is the initial sort key.
    """

    default: bool = False
    descending: bool = False

Widget dataclass

Bases: BasePresentation

Preferred input/display mechanism for the field.

The kind is a semantic name, not an HTML element. Consuming libraries map these to their own components.

Standard kinds (consumers should recognize at minimum): text, textarea, date, datetime, time, color, email, url, password, file, hidden, toggle, slider, rating, rich_text, code, search, phone, currency, autocomplete

Libraries are free to define additional kinds. Unknown kinds should fall back to "text" without raising errors.

Source code in src/airfield/types.py
@dataclass(frozen=True, slots=True)
class Widget(BasePresentation):
    """Preferred input/display mechanism for the field.

    The ``kind`` is a semantic name, not an HTML element.
    Consuming libraries map these to their own components.

    Standard kinds (consumers should recognize at minimum):
        text, textarea, date, datetime, time, color, email, url,
        password, file, hidden, toggle, slider, rating, rich_text,
        code, search, phone, currency, autocomplete

    Libraries are free to define additional kinds. Unknown kinds
    should fall back to ``"text"`` without raising errors.
    """

    kind: str

AirField(default=..., *, primary_key=False, type=None, label=None, widget=None, choices=None, autofocus=False, placeholder=None, help_text=None, default_factory=None, **kwargs)

Unified field descriptor for Pydantic models.

Accepts presentation metadata (primary_key, type, label, widget, choices, placeholder, help_text, autofocus) and all standard pydantic.Field parameters.

All AirField-specific parameters become typed metadata objects in field_info.metadata. Remaining **kwargs pass through to pydantic.Field(); Pydantic raises on unrecognized parameters.

Returns:

Type Description
Any

A Pydantic FieldInfo configured with all specified parameters.

Source code in src/airfield/main.py
def AirField(  # noqa: N802
    default: Any = ...,
    *,
    # Presentation
    primary_key: bool = False,
    type: str | None = None,
    label: str | None = None,
    widget: str | None = None,
    choices: list[tuple[Any, str]] | None = None,
    autofocus: bool = False,
    placeholder: str | None = None,
    help_text: str | None = None,
    # Pydantic pass-through
    default_factory: Any = None,
    **kwargs: Any,
) -> Any:
    """Unified field descriptor for Pydantic models.

    Accepts presentation metadata (``primary_key``, ``type``, ``label``,
    ``widget``, ``choices``, ``placeholder``, ``help_text``,
    ``autofocus``) and all standard ``pydantic.Field`` parameters.

    All AirField-specific parameters become typed metadata objects in
    ``field_info.metadata``. Remaining ``**kwargs`` pass through to
    ``pydantic.Field()``; Pydantic raises on unrecognized parameters.

    Returns:
        A Pydantic FieldInfo configured with all specified parameters.
    """
    if default is not ...:
        kwargs["default"] = default
    if default_factory is not None:
        kwargs["default_factory"] = default_factory

    field_info: FieldInfo = PydanticField(**kwargs)

    # Typed presentation metadata
    if primary_key:
        field_info.metadata.append(PrimaryKey())
    if type:
        field_info.metadata.append(Widget(kind=type))
    if widget:
        field_info.metadata.append(Widget(kind=widget))
    if label:
        field_info.metadata.append(Label(text=label))
    if placeholder:
        field_info.metadata.append(Placeholder(text=placeholder))
    if help_text:
        field_info.metadata.append(HelpText(text=help_text))
    if choices:
        field_info.metadata.append(Choices(*choices))
        # Choices implies a select widget unless explicitly overridden
        if not type and not widget:
            field_info.metadata.append(Widget(kind="select"))
    if autofocus:
        field_info.metadata.append(Autofocus())

    return field_info