From c5625da44a2d79b5cbb20021dc10789134eeed40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez?= Date: Wed, 11 Feb 2026 01:19:06 +0100 Subject: [PATCH] test annotated --- tests/test_annotated_validators.py | 185 +++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 tests/test_annotated_validators.py diff --git a/tests/test_annotated_validators.py b/tests/test_annotated_validators.py new file mode 100644 index 000000000..703ae7e22 --- /dev/null +++ b/tests/test_annotated_validators.py @@ -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": {}}, + } + ] + }