Annotated pattern for custom types with constraints
The Annotated pattern can be used to create reusable types with constraints. For example, `PositiveInt = Annotated[int, Field(gt=0)]` creates a type that validates integers greater than 0. These types can be used with TypeAdapter for validation, as in `ta = TypeAdapter(PositiveInt)` and `ta.validate_python(1)`.
AfterValidator for custom validation and serialization
The AfterValidator marker can be used within Annotated to add custom validation logic. Example: `TruncatedFloat = Annotated[float, AfterValidator(lambda x: round(x, 1))]`. This validates the input first as the base type, then applies the provided function to the validated value.
PlainSerializer for custom serialization
PlainSerializer can be used with Annotated to define custom serialization behavior. Example: `Annotated[float, PlainSerializer(lambda x: f'{x:.1e}', return_type=str)]` will serialize floats as scientific notation strings.
WithJsonSchema marker for custom JSON schemas
WithJsonSchema can be used within Annotated to customize the JSON schema for a type. Example: `Annotated[float, WithJsonSchema({'type': 'string'}, mode='serialization')]` will generate a string type in the serialization JSON schema.
Type variables with Annotated constraints
Type variables can be used within Annotated types to create generic constrained types. Example: `ShortList = Annotated[list[T], Len(max_length=4)]` creates a type that validates lists of any type T with a maximum length of 4 items.
Named type aliases with TypeAliasType
Named type aliases can be created using TypeAliasType (Python 3.10+) or the `type` statement (Python 3.12+). Named aliases differ from implicit aliases in that they are properly represented as definitions in JSON Schema when used multiple times in a model, rather than having their definition duplicated.
Field-specific metadata in named type aliases not supported
Field-specific metadata such as `alias`, `default`, and `deprecated` cannot be used within named type aliases. Named aliases can only contain metadata that applies to the annotated type itself, such as validation constraints and JSON metadata. Using field-specific metadata in a named alias will cause an error.
Named recursive type aliases example
Recursive type aliases must use the named alias pattern. Example: `Json = TypeAliasType('Json', 'dict[str, Json] | list[Json] | str | int | float | bool | None')` creates a recursively-defined JSON type. The annotation must be wrapped in quotes for eager evaluation to be deferred.
__get_pydantic_core_schema__ method on custom types
Custom types can implement `__get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema` to customize validation. This method receives a source_type parameter and a handler callable, and must return a CoreSchema object defining how validation should work.
__get_pydantic_core_schema__ as annotation metadata
The `__get_pydantic_core_schema__` pattern can be implemented on metadata classes used within Annotated, allowing parametrized custom validation without requiring the result to be an instance of a custom class. The method signature is the same: `__get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema`.
Handling third-party types with custom annotations
Third-party types without Pydantic integration can be handled by creating a marker class with `__get_pydantic_core_schema__` and wrapping the type in an Annotated. This allows defining custom validation and serialization for types not under your control.
GetPydanticSchema for reducing boilerplate
GetPydanticSchema provides a simplified way to define custom schemas inline without creating a separate marker class. Example: `Annotated[str, GetPydanticSchema(lambda tp, handler: core_schema.no_info_after_validator_function(lambda x: x * 2, handler(tp)))]`.
Custom generic classes with __get_pydantic_core_schema__
Generic classes can use `__get_pydantic_core_schema__` with `get_args()` and `get_origin()` to extract type parameters and generate appropriate schemas. Use `handler.generate_schema()` to generate schemas for generic parameters instead of `handler()` to avoid context influence.
Field name access in __get_pydantic_core_schema__
As of Pydantic V2.4, the field name can be accessed via `handler.field_name` within `__get_pydantic_core_schema__`, and is available to validators through `info.field_name` in ValidationInfo.
Field name access in AfterValidator
Field name can be accessed from validators used with Annotated markers like AfterValidator. The field name is available via `info.field_name` in the ValidationInfo parameter passed to the validator function.
Pydantic provides built-in markers for common customizations
Pydantic provides high-level hooks to customize types via Annotated, such as AfterValidator, PlainSerializer, and Field. These should be preferred over lower-level approaches when possible.
core_schema module for advanced customization
Advanced customization uses pydantic_core's core_schema module directly. Common functions include core_schema.no_info_after_validator_function(), core_schema.is_instance_schema(), core_schema.chain_schema(), core_schema.union_schema(), and core_schema.json_or_python_schema().
Serialization info parameter in serializers
Both field and model serializers can optionally accept an extra info parameter providing useful information such as user-defined context, current serialization mode ('python' or 'json'), serialization parameters (exclude_unset, serialize_as_any), and for field serializers, the current field name.
Field serializer with context example
from pydantic import BaseModel, FieldSerializationInfo, field_serializer
class Model(BaseModel):
text: str
@field_serializer('text', mode='plain')
@classmethod
def remove_stopwords(cls, v: str, info: FieldSerializationInfo) -> str:
if isinstance(info.context, dict):
stopwords = info.context.get('stopwords', set())
v = ' '.join(w for w in v.split() if w.lower() not in stopwords)
return v
model = Model(text='This is an example document')
print(model.model_dump())
#> {'text': 'This is an example document'}
print(model.model_dump(context={'stopwords': ['this', 'is', 'an']}))
#> {'text': 'example document'}
Subclasses of supported types serialization
Subclasses of supported types (e.g., custom date subclass) are serialized according to their super class. The custom properties defined on the subclass are not included in serialization.
Custom date subclass serialization example
from datetime import date
from pydantic import BaseModel
class MyDate(date):
@property
def my_date_format(self) -> str:
return self.strftime('%d/%m/%Y')
class FooModel(BaseModel):
date: date
m = FooModel(date=MyDate(2023, 1, 1))
print(m.model_dump_json())
#> {"date":"2023-01-01"}