Reputation: 1
I have a model for a search endpoint that looks like this:
class ArtistInboundSearchModel(BaseModel):
ids: Optional[str] = Query(default=None)
name: Optional[str] = Query(default=None)
name_like: Optional[str] = Query(default=None)
page: Optional[int] = Query(default=None)
page_length: Optional[int] = Query(default=None)
As expected, if I post to an endpoint taking this model with "page_length" equal to "dog" AND "page" equal to "cat", I get a 422 response as Pydantic intercepts the request and validates it. This 422 response has 2 errors in it, one for each failed validation, as expected:
Response to "/artists?page=cat&page_length=dog":
{
"detail": [
{
"type": "int_parsing",
"loc": [
"query",
"page"
],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "cat"
},
{
"type": "int_parsing",
"loc": [
"query",
"page_length"
],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "dog"
}
]
}
GREAT. Now let's talk about the query arg "ids". The purpose of this query arg is to allow the requestor to supply a comma-delimited list of ids and restrict the search to those ids. We will need to validate the ids input and make sure each id is a valid guid, otherwise, when we tokenize the string and try to convert them to ids, we'll get an exception.
The only real documentation I found around this suggested doing the following to implement a property validator (validate_comma_delimited_ids and generate_invalid_comma_delimited_ids_message are mine; they are unit tested and work as expected):
@field_validator('ids')
def ids_valid(cls, value: str):
results = validate_comma_delimited_ids(value)
if(results is not None):
message = generate_invalid_comma_delimited_ids_message("ids", results)
raise ValueError(message)
return value
This performs the proper validation, and if it passes, returns an unchanged value for me to tokenize and pass along to BL. If the validation fails, though, instead of adding it to the list of failures, it raises the ValueError and i get a 500 response for unhandled errors.
I looked around for a long time trying to find some information, maybe another parameter that is the rolling list of errors, or something like that and I just cannot.
The desired behavior, and the behavior I would expect from any other modern validation framework or my own validation framework, would be that I could have my validation block add a validation error to a running list so that it worked something like:
Response to "/artists?page=cat&page_length=dog?ids=notanid,ohboy"
{
"detail": [
{
"type": "int_parsing",
"loc": [
"query",
"page"
],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "cat"
},
{
"type": "int_parsing",
"loc": [
"query",
"page_length"
],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "dog"
},
{
"type": "custom_parsing",
"loc": [
"query",
"ids"
],
"msg": "Property should be a comma-delimited list of valid uuidv4 values.",
"input": "notanid,ohboy"
}
]
}
Or really whatever I want. How do I access the rolling list of validation errors in my validation block so I can push failures to it and integrate with Pydantics usual operations?
I want a solution that makes it easy to define custom validation right in the model alongside the other validation definitions, as this is what other frameworks do and also is the preferred developer experience for my devs. I believe that pydantic isn't bad, per se, so I think there must be some way to do this, otherwise it is really not a useful tool for building apis.
Upvotes: 0
Views: 549
Reputation: 1
Good news to anyone having this same issue: it wasn't well documented, and Yurii above is correct that the "normal" way you would solve this with bodies is not supported for query args, but I solved the above issue myself without hacking! Here's how!
def validate_ids(value: str | None):
if value is not None:
common_utilities: CommonUtilities = CommonUtilities()
results: dict[int, str] | None = validate_comma_delimited_ids(value)
if(results is not None):
error_message = f"Property should be a comma-delimited list of valid uuidv4 values."
raise PydanticCustomError(
'invalid_id_list',
'{message}',
dict(
message = error_message
)
)
return value
class ArtistInboundSearchModel(BaseModel):
ids: Annotated[Optional[str], BeforeValidator(validate_ids)] = None
name: Optional[str] = None
name_like: Optional[str] = None
page: Optional[int] = None
page_length: Optional[int] = None
Upvotes: 0
Reputation: 2353
FastAPI doesn't support Pydantic models for Query
parameters. It worked somehow with Pydantic v.1, but was never documented, and it doesn't work with Pydantic v.2.
You can only use Pydantic models for Body
parameters and responses.
As for passing these parameters as Body
parameters, it works the way you expect:
from typing import Optional
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field, field_validator
class ArtistInboundSearchModel(BaseModel):
ids: Optional[str] = Field(default=None)
page: Optional[int] = Field(default=None)
@field_validator('ids')
def ids_valid(cls, value: str):
raise ValueError("-----")
app = FastAPI()
@app.post("/")
def get_index(a: ArtistInboundSearchModel):
pass
def test_index():
with TestClient(app) as client:
resp = client.post("/", json={"page": "cat", "ids": ""})
assert resp.status_code == 422
data = resp.json()
assert len(data["detail"]) == 2, data["detail"]
Upvotes: 0