mirror of https://github.com/tiangolo/fastapi.git
186 lines
5.4 KiB
Python
186 lines
5.4 KiB
Python
"""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": {}},
|
|
}
|
|
]
|
|
}
|