[python] Run regex pattern validators in mode="before" so they see the wire value (#24065)

Merged
SE
4mo ago
+50-125

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 string with format: date-time arrived as a datetime. str(datetime) produces e.g. 2026-05-29 06:25:09.281000+00:00, which no longer matches a pattern written for the RFC 3339 wire form (...T...Z) — so a valid response was rejected.
  • A string with format: uuid was coerced to UUID, then the validator returned str(value), leaving the field value a str despite the UUID annotation.

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 a str — bug reproduced on master then confirmed fixed).
  • Optional None accepted.
  • Malformed string still rejected by the pattern.
  • An already-typed UUID passed to the constructor is preserved (not coerced to str).

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 for str inputs; let pydantic handle conversion afterward.
    • Prevent false negatives for date-time and stop converting UUID values to str.
    • Regenerated python, python-aiohttp, python-httpx, and python-lazyImports; pydantic v1 and fastapi templates are unchanged.