1 changed files with 185 additions and 0 deletions
@ -0,0 +1,185 @@ |
|||||
|
"""Test Pydantic validators in Annotated work correctly with FastAPI. |
||||
|
|
||||
|
This test ensures that Pydantic v2 validators like AfterValidator, BeforeValidator |
||||
|
work correctly in Annotated types with FastAPI parameters. |
||||
|
|
||||
|
Context: PR #13314 broke AfterValidator support and was reverted. |
||||
|
We need to ensure new changes preserve validator functionality. |
||||
|
""" |
||||
|
|
||||
|
from typing import Annotated |
||||
|
|
||||
|
from fastapi import FastAPI, Query, Body |
||||
|
from fastapi.testclient import TestClient |
||||
|
from pydantic import AfterValidator, BeforeValidator, ValidationError |
||||
|
import pytest |
||||
|
|
||||
|
|
||||
|
def validate_positive(v: int) -> int: |
||||
|
"""Validator that ensures value is positive.""" |
||||
|
if v <= 0: |
||||
|
raise ValueError("must be positive") |
||||
|
return v |
||||
|
|
||||
|
|
||||
|
def double_value(v) -> int: |
||||
|
"""Validator that doubles the input value.""" |
||||
|
# BeforeValidator receives the raw value before Pydantic coercion |
||||
|
# For query params, this is a string |
||||
|
v_int = int(v) if isinstance(v, str) else v |
||||
|
return v_int * 2 |
||||
|
|
||||
|
|
||||
|
def strip_whitespace(v: str) -> str: |
||||
|
"""Validator that strips whitespace.""" |
||||
|
return v.strip() |
||||
|
|
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
|
||||
|
@app.get("/query-after-validator") |
||||
|
def query_after_validator( |
||||
|
value: Annotated[int, AfterValidator(validate_positive), Query()], |
||||
|
) -> int: |
||||
|
return value |
||||
|
|
||||
|
|
||||
|
@app.get("/query-before-validator") |
||||
|
def query_before_validator( |
||||
|
value: Annotated[int, BeforeValidator(double_value), Query()], |
||||
|
) -> int: |
||||
|
return value |
||||
|
|
||||
|
|
||||
|
@app.post("/body-after-validator") |
||||
|
def body_after_validator( |
||||
|
value: Annotated[int, AfterValidator(validate_positive), Body()], |
||||
|
) -> int: |
||||
|
return value |
||||
|
|
||||
|
|
||||
|
@app.post("/body-before-validator") |
||||
|
def body_before_validator( |
||||
|
name: Annotated[str, BeforeValidator(strip_whitespace), Body()], |
||||
|
) -> str: |
||||
|
return name |
||||
|
|
||||
|
|
||||
|
@app.get("/query-multiple-validators") |
||||
|
def query_multiple_validators( |
||||
|
value: Annotated[ |
||||
|
int, |
||||
|
BeforeValidator(double_value), |
||||
|
AfterValidator(validate_positive), |
||||
|
Query(), |
||||
|
], |
||||
|
) -> int: |
||||
|
return value |
||||
|
|
||||
|
|
||||
|
client = TestClient(app) |
||||
|
|
||||
|
|
||||
|
def test_query_after_validator_valid(): |
||||
|
"""Test AfterValidator in Query with valid value.""" |
||||
|
response = client.get("/query-after-validator?value=5") |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == 5 |
||||
|
|
||||
|
|
||||
|
def test_query_after_validator_invalid(): |
||||
|
"""Test AfterValidator in Query with invalid value.""" |
||||
|
response = client.get("/query-after-validator?value=-1") |
||||
|
assert response.status_code == 422, response.text |
||||
|
assert response.json() == { |
||||
|
"detail": [ |
||||
|
{ |
||||
|
"type": "value_error", |
||||
|
"loc": ["query", "value"], |
||||
|
"msg": "Value error, must be positive", |
||||
|
"input": "-1", |
||||
|
"ctx": {"error": {}}, |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def test_query_before_validator(): |
||||
|
"""Test BeforeValidator in Query doubles the value.""" |
||||
|
response = client.get("/query-before-validator?value=5") |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == 10 # 5 * 2 |
||||
|
|
||||
|
|
||||
|
def test_body_after_validator_valid(): |
||||
|
"""Test AfterValidator in Body with valid value.""" |
||||
|
# Body() without embed means send the value directly, not wrapped in an object |
||||
|
response = client.post("/body-after-validator", json=10) |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == 10 |
||||
|
|
||||
|
|
||||
|
def test_body_after_validator_invalid(): |
||||
|
"""Test AfterValidator in Body with invalid value.""" |
||||
|
response = client.post("/body-after-validator", json=-5) |
||||
|
assert response.status_code == 422, response.text |
||||
|
assert response.json() == { |
||||
|
"detail": [ |
||||
|
{ |
||||
|
"type": "value_error", |
||||
|
"loc": ["body"], |
||||
|
"msg": "Value error, must be positive", |
||||
|
"input": -5, |
||||
|
"ctx": {"error": {}}, |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def test_body_before_validator(): |
||||
|
"""Test BeforeValidator in Body strips whitespace.""" |
||||
|
response = client.post("/body-before-validator", json=" hello ") |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == "hello" |
||||
|
|
||||
|
|
||||
|
def test_query_multiple_validators(): |
||||
|
"""Test multiple validators work in correct order.""" |
||||
|
# Input: 3 → BeforeValidator doubles to 6 → AfterValidator checks positive |
||||
|
response = client.get("/query-multiple-validators?value=3") |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == 6 |
||||
|
|
||||
|
# Input: -1 → BeforeValidator doubles to -2 → AfterValidator fails |
||||
|
response = client.get("/query-multiple-validators?value=-1") |
||||
|
assert response.status_code == 422, response.text |
||||
|
assert response.json() == { |
||||
|
"detail": [ |
||||
|
{ |
||||
|
"type": "value_error", |
||||
|
"loc": ["query", "value"], |
||||
|
"msg": "Value error, must be positive", |
||||
|
"input": "-1", |
||||
|
"ctx": {"error": {}}, |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def test_query_multiple_validators_zero(): |
||||
|
"""Test multiple validators with zero (edge case).""" |
||||
|
# Input: 0 → BeforeValidator doubles to 0 → AfterValidator fails (not positive) |
||||
|
response = client.get("/query-multiple-validators?value=0") |
||||
|
assert response.status_code == 422, response.text |
||||
|
assert response.json() == { |
||||
|
"detail": [ |
||||
|
{ |
||||
|
"type": "value_error", |
||||
|
"loc": ["query", "value"], |
||||
|
"msg": "Value error, must be positive", |
||||
|
"input": "0", |
||||
|
"ctx": {"error": {}}, |
||||
|
} |
||||
|
] |
||||
|
} |
||||
Loading…
Reference in new issue