Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/en/docs/tutorial/query-params-str-validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,68 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t

{* ../../docs_src/query_params_str_validations/tutorial014_an_py310.py hl[10] *}

## Custom Validation

There could be cases where you need to do some **custom validation** that can't be done with the parameters shown above.

In those cases, you can use a **custom validator function** that is applied after the normal validation (e.g. after validating that the value is a `str`).

You can achieve that using <a href="https://docs.pydantic.dev/latest/concepts/validators/#field-after-validator" class="external-link" target="_blank">Pydantic's `AfterValidator`</a> inside of `Annotated`.

/// tip

Pydantic also has <a href="https://docs.pydantic.dev/latest/concepts/validators/#field-before-validator" class="external-link" target="_blank">`BeforeValidator`</a> and others. πŸ€“

///

For example, this custom validator checks that the item ID starts with `isbn-` for an <abbr title="ISBN means International Standard Book Number">ISBN</abbr> book number or with `imdb-` for an <abbr title="IMDB (Internet Movie Database) is a website with information about movies">IMDB</abbr> movie URL ID:

{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py hl[5,16:19,24] *}

/// info

This is available with Pydantic version 2 or above. 😎

///

/// tip

If you need to do any type of validation that requires communicating with any **external component**, like a database or another API, you should instead use **FastAPI Dependencies**, you will learn about them later.

These custom validators are for things that can be checked with **only** the **same data** provided in the request.

///

### Understand that Code

The important point is just using **`AfterValidator` with a function inside `Annotated`**. Feel free to skip this part. 🀸

---

But if you're curious about this specific code example and you're still entertained, here are some extra details.

#### String with `value.startswith()`

Did you notice? a string using `value.startswith()` can take a tuple, and it will check each value in the tuple:

{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[16:19] hl[17] *}

#### A Random Item

With `data.items()` we get an <abbr title="Something we can iterate on with a for loop, like a list, set, etc.">iterable object</abbr> with tuples containing the key and value for each dictionary item.

We convert this iterable object into a proper `list` with `list(data.items())`.

Then with `random.choice()` we can get a **random value** from the list, so, we get a tuple with `(id, name)`. It will be something like `("imdb-tt0371724", "The Hitchhiker's Guide to the Galaxy")`.

Then we **assign those two values** of the tuple to the variables `id` and `name`.

So, if the user didn't provide an item ID, they will still receive a random suggestion.

...we do all this in a **single simple line**. 🀯 Don't you love Python? 🐍

{* ../../docs_src/query_params_str_validations/tutorial015_an_py310.py ln[22:30] hl[29] *}

## Recap

You can declare additional validations and metadata for your parameters.
Expand All @@ -423,6 +485,8 @@ Validations specific for strings:
* `max_length`
* `pattern`

Custom validations using `AfterValidator`.

In these examples you saw how to declare validations for `str` values.

See the next chapters to learn how to declare validations for other types, like numbers.
31 changes: 31 additions & 0 deletions docs_src/query_params_str_validations/tutorial015_an.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import random
from typing import Union

from fastapi import FastAPI
from pydantic import AfterValidator
from typing_extensions import Annotated

app = FastAPI()

data = {
"isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
"imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
"isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
if not id.startswith(("isbn-", "imdb-")):
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
return id


@app.get("/items/")
async def read_items(
id: Annotated[Union[str, None], AfterValidator(check_valid_id)] = None,
):
if id:
item = data.get(id)
else:
id, item = random.choice(list(data.items()))
return {"id": id, "name": item}
30 changes: 30 additions & 0 deletions docs_src/query_params_str_validations/tutorial015_an_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import random
from typing import Annotated

from fastapi import FastAPI
from pydantic import AfterValidator

app = FastAPI()

data = {
"isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
"imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
"isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
if not id.startswith(("isbn-", "imdb-")):
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
return id


@app.get("/items/")
async def read_items(
id: Annotated[str | None, AfterValidator(check_valid_id)] = None,
):
if id:
item = data.get(id)
else:
id, item = random.choice(list(data.items()))
return {"id": id, "name": item}
30 changes: 30 additions & 0 deletions docs_src/query_params_str_validations/tutorial015_an_py39.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import random
from typing import Annotated, Union

from fastapi import FastAPI
from pydantic import AfterValidator

app = FastAPI()

data = {
"isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
"imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
"isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
if not id.startswith(("isbn-", "imdb-")):
raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
return id


@app.get("/items/")
async def read_items(
id: Annotated[Union[str, None], AfterValidator(check_valid_id)] = None,
):
if id:
item = data.get(id)
else:
id, item = random.choice(list(data.items()))
return {"id": id, "name": item}
8 changes: 4 additions & 4 deletions fastapi/dependencies/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,15 +449,15 @@ def analyze_param(
# We might check here that `default_value is RequiredParam`, but the fact is that the same
# parameter might sometimes be a path parameter and sometimes not. See
# `tests/test_infer_param_optionality.py` for an example.
field_info = params.Path(annotation=type_annotation)
field_info = params.Path(annotation=use_annotation)
elif is_uploadfile_or_nonable_uploadfile_annotation(
type_annotation
) or is_uploadfile_sequence_annotation(type_annotation):
field_info = params.File(annotation=type_annotation, default=default_value)
field_info = params.File(annotation=use_annotation, default=default_value)
elif not field_annotation_is_scalar(annotation=type_annotation):
field_info = params.Body(annotation=type_annotation, default=default_value)
field_info = params.Body(annotation=use_annotation, default=default_value)
else:
field_info = params.Query(annotation=type_annotation, default=default_value)
field_info = params.Query(annotation=use_annotation, default=default_value)

field = None
# It's a field_info, not a dependency
Expand Down
22 changes: 0 additions & 22 deletions tests/test_analyze_param.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import importlib

import pytest
from dirty_equals import IsStr
from fastapi.testclient import TestClient
from inline_snapshot import snapshot

from ...utils import needs_py39, needs_py310, needs_pydanticv2


@pytest.fixture(
name="client",
params=[
pytest.param("tutorial015_an", marks=needs_pydanticv2),
pytest.param("tutorial015_an_py310", marks=(needs_py310, needs_pydanticv2)),
pytest.param("tutorial015_an_py39", marks=(needs_py39, needs_pydanticv2)),
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(
f"docs_src.query_params_str_validations.{request.param}"
)

client = TestClient(mod.app)
return client


def test_get_random_item(client: TestClient):
response = client.get("/items")
assert response.status_code == 200, response.text
assert response.json() == {"id": IsStr(), "name": IsStr()}


def test_get_item(client: TestClient):
response = client.get("/items?id=isbn-9781529046137")
assert response.status_code == 200, response.text
assert response.json() == {
"id": "isbn-9781529046137",
"name": "The Hitchhiker's Guide to the Galaxy",
}


def test_get_item_does_not_exist(client: TestClient):
response = client.get("/items?id=isbn-nope")
assert response.status_code == 200, response.text
assert response.json() == {"id": "isbn-nope", "name": None}


def test_get_invalid_item(client: TestClient):
response = client.get("/items?id=wtf-yes")
assert response.status_code == 422, response.text
assert response.json() == snapshot(
{
"detail": [
{
"type": "value_error",
"loc": ["query", "id"],
"msg": 'Value error, Invalid ID format, it must start with "isbn-" or "imdb-"',
"input": "wtf-yes",
"ctx": {"error": {}},
}
]
}
)


def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"name": "id",
"in": "query",
"required": False,
"schema": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"title": "Id",
},
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail",
}
},
"type": "object",
"title": "HTTPValidationError",
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}]
},
"type": "array",
"title": "Location",
},
"msg": {"type": "string", "title": "Message"},
"type": {"type": "string", "title": "Error Type"},
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError",
},
}
},
}
)