Skip to content

Spec Adapters

Adapters convert various input specifications into a normalized format for parsing.

Learn how to create custom adapters in the Advanced Usage guide.

The following built-in adapters are not meant to be used directly. They serve more as an example than anything else.

anyschema.adapters

attrs_adapter(spec: AttrsClassType) -> FieldSpecIterable

Adapter for attrs classes.

Extracts field information from an attrs class and converts it into an iterator yielding field information as (field_name, field_type, constraints, metadata) tuples.

Parameters:

Name Type Description Default
spec AttrsClassType

An attrs class (not an instance).

required

Yields:

Type Description
FieldSpecIterable

A tuple of (field_name, field_type, constraints, metadata) for each field. - field_name: The name of the field as defined in the attrs class - field_type: The type annotation of the field - constraints: Always empty tuple (attrs doesn't use constraints) - metadata: A dict of custom metadata from the field's metadata dict

Examples:

>>> from attrs import define, field
>>>
>>> @define
... class Student:
...     name: str
...     age: int = field(metadata={"description": "Student age"})
>>>
>>> list(attrs_adapter(Student))
[('name', <class 'str'>, (), {}), ('age', <class 'int'>, (), {'description': 'Student age'})]
Source code in anyschema/adapters.py
def attrs_adapter(spec: AttrsClassType) -> FieldSpecIterable:
    """Adapter for attrs classes.

    Extracts field information from an attrs class and converts it into an iterator
    yielding field information as `(field_name, field_type, constraints, metadata)` tuples.

    Arguments:
        spec: An attrs class (not an instance).

    Yields:
        A tuple of `(field_name, field_type, constraints, metadata)` for each field.
            - `field_name`: The name of the field as defined in the attrs class
            - `field_type`: The type annotation of the field
            - `constraints`: Always empty tuple (attrs doesn't use constraints)
            - `metadata`: A dict of custom metadata from the field's metadata dict

    Examples:
        >>> from attrs import define, field
        >>>
        >>> @define
        ... class Student:
        ...     name: str
        ...     age: int = field(metadata={"description": "Student age"})
        >>>
        >>> list(attrs_adapter(Student))
        [('name', <class 'str'>, (), {}), ('age', <class 'int'>, (), {'description': 'Student age'})]
    """
    import attrs

    # get_type_hints eagerly evaluates annotations, which alleviates us from
    # needing to evaluate ForwardRef's by hand later on.
    # However, it may fail for classes defined in local scopes (e.g., nested classes in functions)
    # so we fall back to using field.type directly if get_type_hints fails.
    try:
        annot_map = get_type_hints(spec)
    except Exception:  # pragma: no cover  # noqa: BLE001
        # If we can't get type hints, use field.type directly
        annot_map = {}

    attrs_fields = attrs.fields(spec)
    attrs_field_names = {field.name for field in attrs_fields}

    # Check for annotations that aren't attrs fields
    # This can happen when a class inherits from an attrs class but isn't decorated itself
    if annot_map and (missing_fields := tuple(field for field in annot_map if field not in attrs_field_names)):
        missing_str = ", ".join(f"'{f}'" for f in sorted(missing_fields))
        msg = (
            f"Class '{spec.__name__}' has annotations ({missing_str}) that are not attrs fields. "
            f"If this class inherits from an attrs class, you must also decorate it with @attrs.define "
            f"or @attrs.frozen to properly define these fields."
        )
        raise AssertionError(msg)

    for field in attrs_fields:
        field_name = field.name
        field_type = annot_map.get(field_name, field.type)

        # Extract metadata if present - attrs stores it as a mapping
        # Create a copy to avoid mutating the original attrs field metadata
        metadata = dict(field.metadata) if field.metadata else {}

        yield field_name, field_type, (), metadata

dataclass_adapter(spec: DataclassType) -> FieldSpecIterable

Adapter for dataclasses.

Converts a dataclass into an iterator yielding field information as (field_name, field_type, constraints, metadata) tuples.

Parameters:

Name Type Description Default
spec DataclassType

A dataclass with annotated fields.

required

Yields:

Type Description
FieldSpecIterable

A tuple of (field_name, field_type, constraints, metadata) for each field.

FieldSpecIterable

Constraints are always empty, and metadata is extracted from dataclass field.metadata.

Examples:

>>> from dataclasses import dataclass, field
>>>
>>> @dataclass
... class Student:
...     name: str
...     age: int = field(metadata={"description": "Student age"})
>>>
>>> list(dataclass_adapter(Student))
[('name', <class 'str'>, (), {}), ('age', <class 'int'>, (), {'description': 'Student age'})]
Source code in anyschema/adapters.py
def dataclass_adapter(spec: DataclassType) -> FieldSpecIterable:
    """Adapter for dataclasses.

    Converts a dataclass into an iterator yielding field information as
    `(field_name, field_type, constraints, metadata)` tuples.

    Arguments:
        spec: A dataclass with annotated fields.

    Yields:
        A tuple of `(field_name, field_type, constraints, metadata)` for each field.
        Constraints are always empty, and metadata is extracted from dataclass field.metadata.

    Examples:
        >>> from dataclasses import dataclass, field
        >>>
        >>> @dataclass
        ... class Student:
        ...     name: str
        ...     age: int = field(metadata={"description": "Student age"})
        >>>
        >>> list(dataclass_adapter(Student))
        [('name', <class 'str'>, (), {}), ('age', <class 'int'>, (), {'description': 'Student age'})]
    """
    # get_type_hints eagerly evaluates annotations, which alleviates us from
    #  needing to evaluate ForwardRef's by hand later on.
    annot_map = get_type_hints(spec)

    # Get dataclass fields
    dataclass_fields = dc_fields(spec)
    dataclass_field_names = {field.name for field in dataclass_fields}

    # Check for annotations that aren't dataclass fields
    # This can happen when a class inherits from a dataclass but isn't decorated itself
    if missing_fields := tuple(field for field in annot_map if field not in dataclass_field_names):
        missing_str = ", ".join(f"'{f}'" for f in missing_fields)
        msg = (
            f"Class '{spec.__name__}' has annotations ({missing_str}) that are not dataclass fields. "
            f"If this class inherits from a dataclass, you must also decorate it with @dataclass "
            f"to properly define these fields."
        )
        raise AssertionError(msg)

    for field in dataclass_fields:
        # Extract metadata dict from dataclass field
        # Create a copy to avoid mutating the original dataclass field metadata
        metadata = dict(field.metadata) if field.metadata else {}

        # Python 3.14+ dataclass fields have a doc parameter
        # Check if field has doc attribute and if it's not None
        if (doc := getattr(field, "doc", None)) and (get_anyschema_value_by_key(metadata, key="description") is None):
            set_anyschema_meta(metadata, key="description", value=doc)

        yield field.name, annot_map[field.name], (), metadata

into_ordered_dict_adapter(spec: IntoOrderedDict) -> FieldSpecIterable

Adapter for Python mappings and sequences of field definitions.

Converts a mapping (e.g., dict) or sequence of 2-tuples into an iterator yielding field information as (field_name, field_type, constraints, metadata) tuples.

Parameters:

Name Type Description Default
spec IntoOrderedDict

A mapping from field names to types, or a sequence of (name, type) tuples.

required

Yields:

Type Description
FieldSpecIterable

A tuple of (field_name, field_type, constraints, metadata) for each field.

FieldSpecIterable

Both constraints and metadata are always empty for this adapter.

Examples:

>>> list(into_ordered_dict_adapter({"name": str, "age": int}))
[('name', <class 'str'>, (), {}), ('age', <class 'int'>, (), {})]
>>> list(into_ordered_dict_adapter([("age", int), ("name", str)]))
[('age', <class 'int'>, (), {}), ('name', <class 'str'>, (), {})]
Source code in anyschema/adapters.py
def into_ordered_dict_adapter(spec: IntoOrderedDict) -> FieldSpecIterable:
    """Adapter for Python mappings and sequences of field definitions.

    Converts a mapping (e.g., `dict`) or sequence of 2-tuples into an iterator yielding field information as
    `(field_name, field_type, constraints, metadata)` tuples.

    Arguments:
        spec: A mapping from field names to types, or a sequence of `(name, type)` tuples.

    Yields:
        A tuple of `(field_name, field_type, constraints, metadata)` for each field.
        Both constraints and metadata are always empty for this adapter.

    Examples:
        >>> list(into_ordered_dict_adapter({"name": str, "age": int}))
        [('name', <class 'str'>, (), {}), ('age', <class 'int'>, (), {})]

        >>> list(into_ordered_dict_adapter([("age", int), ("name", str)]))
        [('age', <class 'int'>, (), {}), ('name', <class 'str'>, (), {})]
    """
    for field_name, field_type in OrderedDict(spec).items():
        yield field_name, field_type, (), {}

pydantic_adapter(spec: type[BaseModel]) -> FieldSpecIterable

Adapter for Pydantic BaseModel classes.

Extracts field information from a Pydantic model class and converts it into an iterator yielding field information as (field_name, field_type, constraints, metadata) tuples.

Parameters:

Name Type Description Default
spec type[BaseModel]

A Pydantic BaseModel class (not an instance).

required

Yields:

Type Description
FieldSpecIterable

A tuple of (field_name, field_type, constraints, metadata) for each field. - field_name: The name of the field as defined in the model - field_type: The type annotation of the field - constraints: A tuple of constraint items from Annotated types (e.g., Gt, Le) - metadata: A dict of custom metadata from json_schema_extra

Examples:

>>> from pydantic import BaseModel, Field
>>> from typing import Annotated
>>>
>>> class Student(BaseModel):
...     name: str = Field(description="Student name")
...     age: Annotated[int, Field(ge=0)]
>>>
>>> spec_fields = list(pydantic_adapter(Student))
>>> spec_fields[0]
('name', <class 'str'>, (), {'anyschema': {'description': 'Student name'}})
>>> spec_fields[1]
('age', ForwardRef('Annotated[int, Field(ge=0)]', is_class=True), (), {})
Source code in anyschema/adapters.py
def pydantic_adapter(spec: type[BaseModel]) -> FieldSpecIterable:
    """Adapter for Pydantic BaseModel classes.

    Extracts field information from a Pydantic model class and converts it into an iterator
    yielding field information as `(field_name, field_type, constraints, metadata)` tuples.

    Arguments:
        spec: A Pydantic `BaseModel` class (not an instance).

    Yields:
        A tuple of `(field_name, field_type, constraints, metadata)` for each field.
            - `field_name`: The name of the field as defined in the model
            - `field_type`: The type annotation of the field
            - `constraints`: A tuple of constraint items from `Annotated` types (e.g., `Gt`, `Le`)
            - `metadata`: A dict of custom metadata from `json_schema_extra`

    Examples:
        >>> from pydantic import BaseModel, Field
        >>> from typing import Annotated
        >>>
        >>> class Student(BaseModel):
        ...     name: str = Field(description="Student name")
        ...     age: Annotated[int, Field(ge=0)]
        >>>
        >>> spec_fields = list(pydantic_adapter(Student))
        >>> spec_fields[0]
        ('name', <class 'str'>, (), {'anyschema': {'description': 'Student name'}})
        >>> spec_fields[1]
        ('age', ForwardRef('Annotated[int, Field(ge=0)]', is_class=True), (), {})
    """
    for field_name, field_info in spec.model_fields.items():
        # Extract constraints from metadata (these are the annotated-types constraints)
        constraints = tuple(field_info.metadata)

        json_schema_extra = field_info.json_schema_extra
        # Create a copy of metadata to avoid mutating the original Pydantic Field
        metadata = dict(json_schema_extra) if json_schema_extra and not callable(json_schema_extra) else {}
        # Extract description from Pydantic Field if present and not already in metadata
        if (description := field_info.description) is not None and (
            get_anyschema_value_by_key(metadata, key="description") is None
        ):
            set_anyschema_meta(metadata, key="description", value=description)

        yield field_name, field_info.annotation, constraints, metadata

sqlalchemy_adapter(spec: SQLAlchemyTableType) -> FieldSpecIterable

Adapter for SQLAlchemy tables.

Extracts field information from a SQLAlchemy Table (Core) or DeclarativeBase class (ORM) and converts it into an iterator yielding field information as (field_name, field_type, metadata) tuples.

Parameters:

Name Type Description Default
spec SQLAlchemyTableType

A SQLAlchemy Table instance or DeclarativeBase subclass.

required

Yields:

Type Description
FieldSpecIterable

A tuple of (field_name, field_type, metadata) for each column. - field_name: The name of the column - field_type: The SQLAlchemy column type - metadata: A tuple containing column metadata (nullable, etc.)

Examples:

>>> from sqlalchemy import Table, Column, Integer, String, MetaData
>>>
>>> metadata = MetaData()
>>> user_table = Table(
...     "user",
...     metadata,
...     Column("id", Integer, primary_key=True),
...     Column("name", String(50)),
... )
>>>
>>> spec_fields = list(sqlalchemy_adapter(user_table))
>>> spec_fields[0]
('id', Integer(), (), {'anyschema': {'nullable': False}})
>>> spec_fields[1]
('name', String(length=50), (), {'anyschema': {'nullable': True}})
>>> from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
>>>
>>> class Base(DeclarativeBase):
...     pass
>>>
>>> class User(Base):
...     __tablename__ = "user"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     name: Mapped[str]
>>>
>>> spec_fields = list(sqlalchemy_adapter(User))
>>> spec_fields[0]
('id', Integer(), (), {'anyschema': {'nullable': False}})
>>> spec_fields[1]
('name', String(length=50), (), {'anyschema': {'nullable': True}})
Source code in anyschema/adapters.py
def sqlalchemy_adapter(spec: SQLAlchemyTableType) -> FieldSpecIterable:
    """Adapter for SQLAlchemy tables.

    Extracts field information from a SQLAlchemy Table (Core) or DeclarativeBase class (ORM)
    and converts it into an iterator yielding field information as `(field_name, field_type, metadata)` tuples.

    Arguments:
        spec: A SQLAlchemy Table instance or DeclarativeBase subclass.

    Yields:
        A tuple of `(field_name, field_type, metadata)` for each column.
            - `field_name`: The name of the column
            - `field_type`: The SQLAlchemy column type
            - `metadata`: A tuple containing column metadata (nullable, etc.)

    Examples:
        >>> from sqlalchemy import Table, Column, Integer, String, MetaData
        >>>
        >>> metadata = MetaData()
        >>> user_table = Table(
        ...     "user",
        ...     metadata,
        ...     Column("id", Integer, primary_key=True),
        ...     Column("name", String(50)),
        ... )
        >>>
        >>> spec_fields = list(sqlalchemy_adapter(user_table))
        >>> spec_fields[0]
        ('id', Integer(), (), {'anyschema': {'nullable': False}})
        >>> spec_fields[1]
        ('name', String(length=50), (), {'anyschema': {'nullable': True}})

        >>> from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column  # doctest: +SKIP
        >>>
        >>> class Base(DeclarativeBase):  # doctest: +SKIP
        ...     pass  # doctest: +SKIP
        >>>
        >>> class User(Base):  # doctest: +SKIP
        ...     __tablename__ = "user"  # doctest: +SKIP
        ...     id: Mapped[int] = mapped_column(primary_key=True)  # doctest: +SKIP
        ...     name: Mapped[str]  # doctest: +SKIP
        >>>
        >>> spec_fields = list(sqlalchemy_adapter(User))  # doctest: +SKIP
        >>> spec_fields[0]  # doctest: +SKIP
        ('id', Integer(), (), {'anyschema': {'nullable': False}})
        >>> spec_fields[1]  # doctest: +SKIP
        ('name', String(length=50), (), {'anyschema': {'nullable': True}})
    """
    from sqlalchemy import Table

    table = spec if isinstance(spec, Table) else spec.__table__

    meta_mapping: dict[Literal["nullable", "unique", "description"], Literal["nullable", "unique", "doc"]] = {
        "nullable": "nullable",
        "unique": "unique",
        "description": "doc",
    }

    for column in table.columns:
        # Create a copy of column.info to avoid mutating the original SQLAlchemy column
        metadata = dict(column.info)

        # Extract anyschema metadata from SQLAlchemy column attributes
        for key, column_attr in meta_mapping.items():
            if (value := getattr(column, column_attr, None)) is not None and (
                get_anyschema_value_by_key(metadata, key=key) is None
            ):
                set_anyschema_meta(metadata, key=key, value=value)

        yield (column.name, column.type, (), metadata)

typed_dict_adapter(spec: TypedDictType) -> FieldSpecIterable

Adapter for TypedDict classes.

Converts a TypedDict into an iterator yielding field information as (field_name, field_type, constraints, metadata) tuples.

Parameters:

Name Type Description Default
spec TypedDictType

A TypedDict class (not an instance).

required

Yields:

Type Description
FieldSpecIterable

A tuple of (field_name, field_type, constraints, metadata) for each field.

FieldSpecIterable

Both constraints and metadata are always empty for this adapter.

Examples:

>>> from typing_extensions import TypedDict
>>>
>>> class Student(TypedDict):
...     name: str
...     age: int
>>>
>>> list(typed_dict_adapter(Student))
[('name', <class 'str'>, (), {}), ('age', <class 'int'>, (), {})]
Source code in anyschema/adapters.py
def typed_dict_adapter(spec: TypedDictType) -> FieldSpecIterable:
    """Adapter for TypedDict classes.

    Converts a TypedDict into an iterator yielding field information as
    `(field_name, field_type, constraints, metadata)` tuples.

    Arguments:
        spec: A TypedDict class (not an instance).

    Yields:
        A tuple of `(field_name, field_type, constraints, metadata)` for each field.
        Both constraints and metadata are always empty for this adapter.

    Examples:
        >>> from typing_extensions import TypedDict
        >>>
        >>> class Student(TypedDict):
        ...     name: str
        ...     age: int
        >>>
        >>> list(typed_dict_adapter(Student))
        [('name', <class 'str'>, (), {}), ('age', <class 'int'>, (), {})]
    """
    type_hints = get_type_hints(spec)
    for field_name, field_type in type_hints.items():
        yield field_name, field_type, (), {}

Adapters specification

Adapters must follow this signature:

from typing import Iterator, TypeAlias, Callable, Any, Generator
from anyschema.typing import FieldConstraints, FieldMetadata, FieldName, FieldType

FieldSpec: TypeAlias = tuple[FieldName, FieldType, FieldConstraints, FieldMetadata]


def my_custom_adapter(spec: Any) -> Iterator[FieldSpec]:
    """
    Yields tuples of (field_name, field_type, constraints, metadata).

    - name (str): The name of the field
    - type (type): The type annotation of the field
    - constraints (tuple): Type constraints (e.g., Gt(0), Le(100) from annotated-types)
    - metadata (dict): Custom metadata dictionary for additional information
    """
    ...

They don't need to be functions; any callable is acceptable.