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