pythonfastapi

Why FastAPI cannot parse UUID by pattern while Regex101 can?


I have some FastAPI application:

from typing import Annotated

import uvicorn
from fastapi import FastAPI, Query
from pydantic import constr

app = FastAPI()


@app.get("search")
async def search(
    partner_ids: Annotated[
        constr(pattern="((^|[,])[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})+"),
        Query(),
    ] = "ca930bc7-f4cf-4b10-9c6f-7db13b6ab4f0",
):
    pass


if __name__ == "__main__":
    uvicorn.run("uuid_list:app", reload=True, port=8004)

The problem with it is validation of partner_ids does not work:

enter image description here

At the same time regex101 finds uuid:

enter image description here

What's wrong with FastAPI?

Upd. The regex is for comma separated list of uuids.


Solution

  • As @MatsLindh mentioned in the comment you can just use List[type]. In your case it will be:

    @app.get("search")
    async def search(
        partner_ids: Annotated[List[UUID], Query(...)]
    ):
        pass
    

    It won't be a comma separated but at least it work out of a box.

    In your example it seems that you have in issue in regex. Try to set uuid pattern like this:

    UUIDPattern = constr(
        regex=r"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(,[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})*$"
    )
    
    @app.get("/search")
    async def search(
        partner_ids: Annotated[UUIDListPattern], Query()],
    ):
        pass
    

    There is a third option using a schema:

    # Pydantic model for validating the input schema
    class PartnerIDsSchema(BaseModel):
        partner_ids: str
    
        @validator('partner_ids')
        def check_uuids(cls, v):
            uuids = v.split(",")
            for uuid_str in uuids:
                try:
                    UUID(uuid_str.strip())
                except ValueError:
                    raise ValueError(f"Invalid UUID found: {uuid_str}")
            return v
    
    @app.get("/search")
    async def search(
        partner_ids: PartnerIDsSchema = Query(...)
    ):
        pass
    

    But you will need to convert then in actual uuids in the method itself because they will be passed as a single string.