Combining Annotated type with BeforeValidator and optional Field with pattern constraint produces incorrect JSON schema
Author: dpitch40Created Oct 17, 2025Updated Sep 16, 2026
Labelsbug V2topic-annotations
Initial Checks
- I confirm that I'm using Pydantic V2
Description
The example code produces this output:
annotation=str required=True metadata=[_PydanticGeneralMetadata(pattern='^[\\da-fA-F]{32}$')]
{'pattern': '^[\\da-fA-F]{32}$', 'title': 'Str Mandatory', 'type': 'string'}
annotated_mandatory
annotation=str required=True metadata=[_PydanticGeneralMetadata(pattern='^[\\da-fA-F]{32}$'), BeforeValidator(func=<function _convert_bytes_to_str at 0x102ab9e40>, json_schema_input_type=PydanticUndefined)]
{'pattern': '^[\\da-fA-F]{32}$',
'title': 'Annotated Mandatory',
'type': 'string'}
str_optional
annotation=Union[str, NoneType] required=False default=None metadata=[_PydanticGeneralMetadata(pattern='^[\\da-fA-F]{32}$')]
{'anyOf': [{'pattern': '^[\\da-fA-F]{32}$', 'type': 'string'},
{'type': 'null'}],
'default': None,
'title': 'Str Optional'}
annotated_optional
annotation=Union[Annotated[str, BeforeValidator], NoneType] required=False default=None metadata=[_PydanticGeneralMetadata(pattern='^[\\da-fA-F]{32}$')]
{'anyOf': [{'type': 'string'}, {'type': 'null'}],
'default': None,
'title': 'Annotated Optional'}The first three fields all produce the expected JSON schema. I would expect the schema for the annotated_optional field to have the same type definition as the schema for the str_optional field, but somehow the combination of the annotated type and optional field results in the pattern being dropped from the JSON schema, even though it's still present in the model's model_fields.
Example Code
import pprint
from typing import Annotated, Union, Optional
from pydantic import BaseModel, BeforeValidator, Field
regex = r"^[\da-fA-F]{32}$"
def _convert_bytes_to_str(value: Union[str, bytes]) -> str:
if isinstance(value, bytes):
return value.decode("utf8")
return value
bytes_to_str = Annotated[str, BeforeValidator(_convert_bytes_to_str)]
class TestModel(BaseModel):
str_mandatory: str = Field(..., pattern=regex)
annotated_mandatory: bytes_to_str = Field(..., pattern=regex)
str_optional: Optional[str] = Field(None, pattern=regex)
annotated_optional: Optional[bytes_to_str] = Field(None, pattern=regex)
schema = TestModel.model_json_schema()
for key in ("str_mandatory", "annotated_mandatory", "str_optional", "annotated_optional"):
print(key)
print(TestModel.model_fields[key])
pprint.pprint(schema["properties"][key])
print()Python, Pydantic & OS Version
pydantic version: 2.12.0
pydantic-core version: 2.41.1
pydantic-core build: profile=release pgo=false
python version: 3.13.7 (main, Aug 14 2025, 11:12:11) [Clang 17.0.0 (clang-1700.3.19.1)]
platform: macOS-26.0.1-arm64-arm-64bit-Mach-O
related packages: pydantic-settings-2.11.0 mypy-1.18.2 fastapi-0.118.3 email-validator-2.3.0 typing_extensions-4.15.0
commit: unknownSource: pydantic/pydantic