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

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 src/anyschema/adapters.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
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

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 src/anyschema/adapters.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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

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 src/anyschema/adapters.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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

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 src/anyschema/adapters.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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

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 src/anyschema/adapters.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
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

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 src/anyschema/adapters.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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.