test annotated

This commit is contained in:
Javier Sánchez 2026-02-11 01:19:06 +01:00
parent d29fac9e3f
commit c5625da44a
1 changed files with 185 additions and 0 deletions

View File

@ -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": {}},
}
]
}