[python] Run regex pattern validators in mode="before" so they see the wire value (#24065)
Fixes #24065
Problem
The Python (pydantic v2) generator emitted regex @field_validators without a mode, so they defaulted to mode="after" and ran after pydantic had already coerced the incoming value to its declared Python type:
@field_validator('created_at')
def created_at_validate_regular_expression(cls, value):
if not isinstance(value, str):
value = str(value) # <- typed value stringified here
if not re.match(PATTERN, value):
raise ValueError(...)
return value
Because the validator received the already-converted value, two things broke:
- A
stringwithformat: date-timearrived as adatetime.str(datetime)produces e.g.2026-05-29 06:25:09.281000+00:00, which no longer matches apatternwritten for the RFC 3339 wire form (...T...Z) — so a valid response was rejected. - A
stringwithformat: uuidwas coerced toUUID, then the validator returnedstr(value), leaving the field value astrdespite theUUIDannotation.
Fix
Run the pattern check in mode="before" against the raw wire value, and only when it is a str; then let pydantic perform its normal conversion. Already-typed Python values pass through untouched.
@field_validator('created_at', mode="before")
def created_at_validate_regular_expression(cls, value):
if isinstance(value, str) and not re.match(PATTERN, value):
raise ValueError(...)
return value
Single template change: modules/openapi-generator/src/main/resources/python/model_generic.mustache. Affected python samples were regenerated (python, python-aiohttp, python-httpx, python-lazyImports); pydantic-v1 and fastapi use separate templates and are unaffected.
Test evidence
Built the generator (JDK 21) and regenerated the samples — the only generated diff is the validator signature/body, as intended.
Against the regenerated python sample's UuidWithPattern (id: Optional[UUID], with a UUID-v4 pattern), verified with pydantic 2.13:
- Valid UUID-v4 string on the wire → field is a proper
UUID(previously astr— bug reproduced onmasterthen confirmed fixed). - Optional
Noneaccepted. - Malformed string still rejected by the pattern.
- An already-typed
UUIDpassed to the constructor is preserved (not coerced tostr).
Existing sample unit tests test_uuid_with_pattern.py and test_format_test.py pass.
Verification done
(1) No in-flight PR — searched open PRs for python regex/pattern-validator work and cross-checked my own open PRs; none overlap. (2) Issue is unclaimed (no comments). (3) Code-focused — .mustache template + regenerated .py samples. (4) Confirmed the old (uncorrected) template still ships in master. (6) N/A (not a spring-projects repo).
Summary by cubic
Run pattern validators in the Python pydantic v2 generator in mode="before" so patterns check the raw wire string. This prevents false rejections for RFC 3339 date-time and preserves UUID types.
- Bug Fixes
- Generate regex
@field_validator(..., mode="before")and run the check only forstrinputs; letpydantichandle conversion afterward. - Prevent false negatives for
date-timeand stop convertingUUIDvalues tostr. - Regenerated
python,python-aiohttp,python-httpx, andpython-lazyImports;pydanticv1 andfastapitemplates are unchanged.
- Generate regex