mirror of https://github.com/tiangolo/fastapi.git
test annotated
This commit is contained in:
parent
d29fac9e3f
commit
c5625da44a
|
|
@ -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