Типы (Types)
Pydantic использует типы, чтобы задать, как выполнять валидацию и сериализацию. Встроенные и стандартные типы (например int, str, date) можно использовать как есть. Для них можно задавать строгость и ограничения.
Помимо этого Pydantic предоставляет дополнительные типы — в самой библиотеке (например SecretStr) или во внешней pydantic-extra-types. Они реализованы по паттернам из раздела о кастомных типах. Строгость и ограничения к ним не применяются.
В документации по встроенным и стандартным типам описаны поддерживаемые типы: допустимые значения, ограничения валидации и возможность настройки строгости.
См. также таблицу преобразований — сводку допустимых значений по типам.
Ниже речь пойдёт о том, как определять собственные кастомные типы.
Кастомные типы (Custom Types)
Задать кастомный тип можно несколькими способами.
Аннотированный паттерн (Using the annotated pattern)
Аннотированный паттерн позволяет делать типы переиспользуемыми. Например, тип «положительное целое»:
from typing import Annotated
from pydantic import Field, TypeAdapter, ValidationError
PositiveInt = Annotated[int, Field(gt=0)] # (1)!
ta = TypeAdapter(PositiveInt)
print(ta.validate_python(1))
#> 1
try:
ta.validate_python(-1)
except ValidationError as exc:
print(exc)
"""
1 validation error for constrained-int
Input should be greater than 0 [type=greater_than, input_value=-1, input_type=int]
"""
Ограничения можно брать из библиотеки annotated-types, чтобы не привязываться к Pydantic:
from annotated_types import Gt
PositiveInt = Annotated[int, Gt(0)]
- Доступ к имени поля — см. раздел Access to field name ниже.
Добавление валидации и сериализации (Adding validation and serialization)
Валидацию, сериализацию и JSON Schema для произвольного типа можно добавлять или переопределять маркерами Pydantic:
from typing import Annotated
from pydantic import (
AfterValidator,
PlainSerializer,
TypeAdapter,
WithJsonSchema,
)
TruncatedFloat = Annotated[
float,
AfterValidator(lambda x: round(x, 1)),
PlainSerializer(lambda x: f'{x:.1e}', return_type=str),
WithJsonSchema({'type': 'string'}, mode='serialization'),
]
ta = TypeAdapter(TruncatedFloat)
input = 1.02345
assert input != 1.0
assert ta.validate_python(input) == 1.0
assert ta.dump_json(input) == b'"1.0e+00"'
assert ta.json_schema(mode='validation') == {'type': 'number'}
assert ta.json_schema(mode='serialization') == {'type': 'string'}
Дженерики (Generics)
Переменные типов можно использовать внутри Annotated:
from typing import Annotated, TypeVar
from annotated_types import Gt, Len
from pydantic import TypeAdapter, ValidationError
T = TypeVar('T')
ShortList = Annotated[list[T], Len(max_length=4)]
ta = TypeAdapter(ShortList[int])
v = ta.validate_python([1, 2, 3, 4])
assert v == [1, 2, 3, 4]
try:
ta.validate_python([1, 2, 3, 4, 5])
except ValidationError as exc:
print(exc)
"""
1 validation error for list[int]
List should have at most 4 items after validation, not 5 [type=too_long, input_value=[1, 2, 3, 4, 5], input_type=list]
"""
PositiveList = list[Annotated[T, Gt(0)]]
ta = TypeAdapter(PositiveList[float])
v = ta.validate_python([1.0])
assert type(v[0]) is float
try:
ta.validate_python([-1.0])
except ValidationError as exc:
print(exc)
"""
1 validation error for list[constrained-float]
0
Input should be greater than 0 [type=greater_than, input_value=-1.0, input_type=float]
"""
Именованные алиасы типов (Named type aliases)
В примерах выше использовались неявные алиасы типов (присвоенные переменной). В runtime Pydantic не знает имя этой переменной, из‑за чего:
- в большинстве случаев рекурсивные алиасы типов не работают;
- JSON Schema алиаса не превращается в definition (удобно, когда алиас используется в модели больше одного раза).
С помощью type statement (PEP 695) алиасы можно задать так:
Python 3.9+ (TypeAliasType из typing_extensions):
from typing import Annotated
from annotated_types import Gt
from typing_extensions import TypeAliasType
from pydantic import BaseModel
PositiveIntList = TypeAliasType('PositiveIntList', list[Annotated[int, Gt(0)]])
class Model(BaseModel):
x: PositiveIntList
y: PositiveIntList
print(Model.model_json_schema()) # (1)!
"""
{
'$defs': {
'PositiveIntList': {
'items': {'exclusiveMinimum': 0, 'type': 'integer'},
'type': 'array',
}
},
'properties': {
'x': {'$ref': '#/$defs/PositiveIntList'},
'y': {'$ref': '#/$defs/PositiveIntList'},
},
'required': ['x', 'y'],
'title': 'Model',
'type': 'object',
}
"""
- Если бы
PositiveIntListбыл неявным алиасом, его определение дублировалось бы в'x'и'y'.
Python 3.12+ (новый синтаксис):
from typing import Annotated
from annotated_types import Gt
from pydantic import BaseModel
type PositiveIntList = list[Annotated[int, Gt(0)]]
class Model(BaseModel):
x: PositiveIntList
y: PositiveIntList
print(Model.model_json_schema()) # (1)!
"""
{
'$defs': {
'PositiveIntList': {
'items': {'exclusiveMinimum': 0, 'type': 'integer'},
'type': 'array',
}
},
'properties': {
'x': {'$ref': '#/$defs/PositiveIntList'},
'y': {'$ref': '#/$defs/PositiveIntList'},
},
'required': ['x', 'y'],
'title': 'Model',
'type': 'object',
}
"""
- Если бы
PositiveIntListбыл неявным алиасом, его определение дублировалось бы в'x'и'y'.
Когда использовать именованные алиасы
Для статических анализаторов (именованные) алиасы по PEP 695 и неявные алиасы эквивалентны, но Pydantic не обрабатывает метаданные уровня поля внутри именованных алиасов. То есть метаданные вроде alias, default, deprecated использовать нельзя:
Python 3.9+:
from typing import Annotated
from typing_extensions import TypeAliasType
from pydantic import BaseModel, Field
MyAlias = TypeAliasType('MyAlias', Annotated[int, Field(default=1)])
class Model(BaseModel):
x: MyAlias # Так нельзя
Python 3.12+:
from typing import Annotated
from pydantic import BaseModel, Field
type MyAlias = Annotated[int, Field(default=1)]
class Model(BaseModel):
x: MyAlias # Так нельзя
Допускаются только метаданные, применимые к самому аннотированному типу (например ограничения валидации и JSON-метаданные). Поддержка метаданных уровня поля потребовала бы ранней интроспекции value алиаса, и тогда алиас нельзя было бы хранить как определение в JSON Schema.
Как и в неявных алиасах, внутри именованного дженерик-алиаса можно использовать переменные типов:
Примечание
Python 3.9+: ShortList = TypeAliasType('ShortList', Annotated[list[T], Len(max_length=4)], type_params=(T,))
Python 3.12+: type ShortList[T] = Annotated[list[T], Len(max_length=4)]
Именованные рекурсивные типы (Named recursive types)
Именованные алиасы типов нужны, когда вы определяете рекурсивные алиасы типов (1).
- По ряду причин Pydantic не поддерживает неявные рекурсивные алиасы — например, не может разрешать forward annotations между модулями.
Пример типа JSON:
Python 3.9+:
from typing import Union
from typing_extensions import TypeAliasType
from pydantic import TypeAdapter
Json = TypeAliasType(
'Json',
'Union[dict[str, Json], list[Json], str, int, float, bool, None]', # (1)!
)
ta = TypeAdapter(Json)
print(ta.json_schema())
"""
{
'$defs': {
'Json': {
'anyOf': [
{
'additionalProperties': {'$ref': '#/$defs/Json'},
'type': 'object',
},
{'items': {'$ref': '#/$defs/Json'}, 'type': 'array'},
{'type': 'string'},
{'type': 'integer'},
{'type': 'number'},
{'type': 'boolean'},
{'type': 'null'},
]
}
},
'$ref': '#/$defs/Json',
}
"""
- Аннотацию нужно брать в кавычки, так как она вычисляется сразу (а
Jsonещё не определён).
Python 3.12+:
from pydantic import TypeAdapter
type Json = dict[str, Json] | list[Json] | str | int | float | bool | None # (1)!
ta = TypeAdapter(Json)
print(ta.json_schema())
"""
{
'$defs': {
'Json': {
'anyOf': [
...
]
}
},
'$ref': '#/$defs/Json',
}
"""
- Значение именованного алиаса вычисляется лениво, поэтому forward annotations не нужны.
В Pydantic есть удобный тип JsonValue.
Совет
Настройка валидации через get_pydantic_core_schema (Customizing validation with get_pydantic_core_schema)
Для более глубокой настройки обработки кастомных классов (особенно когда класс доступен или от него можно наследоваться) можно реализовать специальный метод __get_pydantic_core_schema__, чтобы задать, как Pydantic строит схему для pydantic-core.
Pydantic использует pydantic-core внутри для валидации и сериализации; это новый API в Pydantic V2, его могут менять в будущем. По возможности лучше опираться на встроенные конструкции: annotated-types, pydantic.Field, BeforeValidator и т.п.
__get_pydantic_core_schema__ можно реализовать и на кастомном типе, и на метаданных для Annotated. В обоих случаях API похож на цепочку middleware и на «wrap»-валидаторы: есть source_type (не обязательно совпадающий с классом, особенно для дженериков) и handler, который можно вызвать с типом — либо для перехода к следующему элементу в Annotated, либо к внутренней генерации схемы Pydantic.
Минимальная реализация без побочных эффектов: вызвать handler с переданным типом и вернуть результат. Можно также изменить тип до вызова handler, изменить core schema после вызова или не вызывать handler вовсе.
Как метод кастомного типа (As a method on a custom type)
Пример типа с __get_pydantic_core_schema__ для настройки валидации (аналог __get_validators__ в Pydantic V1):
from typing import Any
from pydantic_core import CoreSchema, core_schema
from pydantic import GetCoreSchemaHandler, TypeAdapter
class Username(str):
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
return core_schema.no_info_after_validator_function(cls, handler(str))
ta = TypeAdapter(Username)
res = ta.validate_python('abc')
assert isinstance(res, Username)
assert res == 'abc'
Подробнее о настройке JSON Schema для кастомных типов: JSON Schema.
Как аннотация (As an annotation)
Часто нужно параметризовать кастомный тип не только дженерик-параметрами (это делается через систему типов). Или не требуется (или не хочется) создавать экземпляр подкласса — нужен исходный тип с дополнительной валидацией.
Например, собственная реализация аналога pydantic.AfterValidator (см. Adding validation and serialization):
Python 3.9+:
from dataclasses import dataclass
from typing import Annotated, Any, Callable
from pydantic_core import CoreSchema, core_schema
from pydantic import BaseModel, GetCoreSchemaHandler
@dataclass(frozen=True) # (1)!
class MyAfterValidator:
func: Callable[[Any], Any]
def __get_pydantic_core_schema__(
self, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
return core_schema.no_info_after_validator_function(
self.func, handler(source_type)
)
Username = Annotated[str, MyAfterValidator(str.lower)]
class Model(BaseModel):
name: Username
assert Model(name='ABC').name == 'abc' # (2)!
- Анализаторы типов не будут ругаться на присваивание
'ABC'типуUsername, так как не считаютUsernameотдельным типом отstr. frozen=TrueделаетMyAfterValidatorхешируемым; иначе union вродеUsername | Noneприведёт к ошибке.
Python 3.10+ (то же с collections.abc.Callable).
Работа со сторонними типами (Handling third-party types)
Ещё один случай — интеграция со сторонними типами:
from typing import Annotated, Any
from pydantic_core import core_schema
from pydantic import (
BaseModel,
GetCoreSchemaHandler,
GetJsonSchemaHandler,
ValidationError,
)
from pydantic.json_schema import JsonSchemaValue
class ThirdPartyType:
"""
Тип из сторонней библиотеки без интеграции с Pydantic,
без pydantic_core.CoreSchema и т.п.
"""
x: int
def __init__(self):
self.x = 0
class _ThirdPartyTypePydanticAnnotation:
@classmethod
def __get_pydantic_core_schema__(
cls,
_source_type: Any,
_handler: GetCoreSchemaHandler,
) -> core_schema.CoreSchema:
"""
Возвращаем pydantic_core.CoreSchema, где:
* int парсятся в ThirdPartyType с этим int в атрибуте x
* экземпляры ThirdPartyType проходят без изменений
* остальное не проходит валидацию
* сериализация всегда возвращает int
"""
def validate_from_int(value: int) -> ThirdPartyType:
result = ThirdPartyType()
result.x = value
return result
from_int_schema = core_schema.chain_schema([
core_schema.int_schema(),
core_schema.no_info_plain_validator_function(validate_from_int),
])
return core_schema.json_or_python_schema(
json_schema=from_int_schema,
python_schema=core_schema.union_schema([
core_schema.is_instance_schema(ThirdPartyType),
from_int_schema,
]),
serialization=core_schema.plain_serializer_function_ser_schema(
lambda instance: instance.x
),
)
@classmethod
def __get_pydantic_json_schema__(
cls, _core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
return handler(core_schema.int_schema())
PydanticThirdPartyType = Annotated[
ThirdPartyType, _ThirdPartyTypePydanticAnnotation
]
class Model(BaseModel):
third_party_type: PydanticThirdPartyType
m_int = Model(third_party_type=1)
assert isinstance(m_int.third_party_type, ThirdPartyType)
assert m_int.third_party_type.x == 1
assert m_int.model_dump() == {'third_party_type': 1}
instance = ThirdPartyType()
instance.x = 10
m_instance = Model(third_party_type=instance)
assert m_instance.model_dump() == {'third_party_type': 10}
try:
Model(third_party_type='a')
except ValidationError as e:
print(e)
assert Model.model_json_schema() == {
'properties': {'third_party_type': {'title': 'Third Party Type', 'type': 'integer'}},
'required': ['third_party_type'],
'title': 'Model',
'type': 'object',
}
Так можно задать поведение для типов Pandas, NumPy и т.п.
GetPydanticSchema для сокращения кода (Using GetPydanticSchema to reduce boilerplate)
Документация API: pydantic.types.GetPydanticSchema
Маркер-класс из примеров выше даёт много шаблонного кода. В простых случаях его можно сократить с помощью pydantic.GetPydanticSchema:
from typing import Annotated
from pydantic_core import core_schema
from pydantic import BaseModel, GetPydanticSchema
class Model(BaseModel):
y: Annotated[
str,
GetPydanticSchema(
lambda tp, handler: core_schema.no_info_after_validator_function(
lambda x: x * 2, handler(tp)
)
),
]
assert Model(y='ab').y == 'abab'
Итог (Summary)
- Для кастомного типа можно реализовать
__get_pydantic_core_schema__на самом типе. - Под капотом используется
pydantic-core; к нему можно подключаться черезGetPydanticSchemaили маркер-класс с__get_pydantic_core_schema__. - У Pydantic есть высокоуровневые хуки для типов через
Annotated—AfterValidator,Fieldи др. Ими стоит пользоваться, когда возможно.
Работа с кастомными дженерик-классами (Handling custom generic classes)
Это продвинутый приём; на первых порах он может не понадобиться. Часто достаточно обычных моделей Pydantic.
Предупреждение
Дженерик-классы можно использовать как типы полей и настраивать валидацию по «параметрам типа» (подтипам) через __get_pydantic_core_schema__.
Если у дженерик-класса есть класс-метод __get_pydantic_core_schema__, для его работы не нужен arbitrary_types_allowed.
Параметр source_type не совпадает с cls; параметры дженерика можно извлечь через typing.get_args (или typing_extensions.get_args). Затем по ним строят схему, вызывая handler.generate_schema. Важно не делать что-то вроде handler(get_args(source_type)[0]), а именно handler.generate_schema(...) — чтобы схема для параметра типа не зависела от текущего контекста метаданных Annotated.
Python 3.9+ / Python 3.10+ (импорт get_args, get_origin из typing_extensions или typing):
from dataclasses import dataclass
from typing import Any, Generic, TypeVar
from pydantic_core import CoreSchema, core_schema
from typing_extensions import get_args, get_origin
from pydantic import (
BaseModel,
GetCoreSchemaHandler,
ValidationError,
ValidatorFunctionWrapHandler,
)
ItemType = TypeVar('ItemType')
@dataclass
class Owner(Generic[ItemType]):
name: str
item: ItemType
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
origin = get_origin(source_type)
if origin is None:
origin = source_type
item_tp = Any
else:
item_tp = get_args(source_type)[0]
item_schema = handler.generate_schema(item_tp)
def val_item(v: Owner[Any], handler: ValidatorFunctionWrapHandler) -> Owner[Any]:
v.item = handler(v.item)
return v
python_schema = core_schema.chain_schema([
core_schema.is_instance_schema(cls),
core_schema.no_info_wrap_validator_function(val_item, item_schema),
])
return core_schema.json_or_python_schema(
json_schema=core_schema.chain_schema([
core_schema.typed_dict_schema({
'name': core_schema.typed_dict_field(core_schema.str_schema()),
'item': core_schema.typed_dict_field(item_schema),
}),
core_schema.no_info_before_validator_function(
lambda data: Owner(name=data['name'], item=data['item']),
python_schema,
),
]),
python_schema=python_schema,
)
class Car(BaseModel):
color: str
class House(BaseModel):
rooms: int
class Model(BaseModel):
car_owner: Owner[Car]
home_owner: Owner[House]
model = Model(
car_owner=Owner(name='John', item=Car(color='black')),
home_owner=Owner(name='James', item=House(rooms=3)),
)
# При неверных подтипах — ValidationError
# Аналогично при model_validate_json с перепутанными item
Дженерик-контейнеры (Generic containers)
Та же идея для дженерик-контейнеров (например, кастомный тип в духе Sequence):
from collections.abc import Sequence
from typing import Any, TypeVar
from pydantic_core import ValidationError, core_schema
from typing import get_args # или typing_extensions.get_args
from pydantic import BaseModel, GetCoreSchemaHandler
T = TypeVar('T')
class MySequence(Sequence[T]):
def __init__(self, v: Sequence[T]):
self.v = v
def __getitem__(self, i):
return self.v[i]
def __len__(self):
return len(self.v)
@classmethod
def __get_pydantic_core_schema__(
cls, source: Any, handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
instance_schema = core_schema.is_instance_schema(cls)
args = get_args(source)
if args:
sequence_t_schema = handler.generate_schema(Sequence[args[0]])
else:
sequence_t_schema = handler.generate_schema(Sequence)
non_instance_schema = core_schema.no_info_after_validator_function(
MySequence, sequence_t_schema
)
return core_schema.union_schema([instance_schema, non_instance_schema])
class M(BaseModel):
model_config = dict(validate_default=True)
s1: MySequence = [3]
m = M()
print(m.s1.v) # [3]
class M(BaseModel):
s1: MySequence[int]
M(s1=[1])
# M(s1=['a']) -> ValidationError
Доступ к имени поля (Access to field name)
В Pydantic V2 до V2.3 этого не было, возвращено в V2.4.
Примечание
Начиная с Pydantic V2.4, внутри __get_pydantic_core_schema__ доступно имя поля через handler.field_name; оно попадает в info.field_name.
from typing import Any
from pydantic_core import core_schema
from pydantic import BaseModel, GetCoreSchemaHandler, ValidationInfo
class CustomType:
"""Кастомный тип, сохраняющий имя поля, в котором использован."""
def __init__(self, value: int, field_name: str):
self.value = value
self.field_name = field_name
def __repr__(self):
return f'CustomType<{self.value} {self.field_name!r}>'
@classmethod
def validate(cls, value: int, info: ValidationInfo):
return cls(value, info.field_name)
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.with_info_after_validator_function(
cls.validate, handler(int)
)
class MyModel(BaseModel):
my_field: CustomType
m = MyModel(my_field=1)
print(m.my_field)
#> CustomType<1 'my_field'>
К field_name можно обращаться и из маркеров в Annotated, например AfterValidator:
from typing import Annotated
from pydantic import AfterValidator, BaseModel, ValidationInfo
def my_validators(value: int, info: ValidationInfo):
return f'<{value} {info.field_name!r}>'
class MyModel(BaseModel):
my_field: Annotated[int, AfterValidator(my_validators)]
m = MyModel(my_field=1)
print(m.my_field)
#> <1 'my_field'>