Перейти к содержанию

JSON

Чтение тела запроса как JSON.

Описание

JSON (JavaScript Object Notation) (MIME-type: application/json) — текстовый формат обмена структурированными данными, основанный на JavaScript. Сегодня JSON является языконезависимым и используется в различных языках программирования.

Этот раздел покажет, как извлекать JSON из тела запроса и валидировать его с помощью Rapidy.

Типы данных в JSON

Объект

Неупорядоченное множество пар «ключ:значение».

{
    "username": "User",
    "password": "myAwesomePass"
}
from pydantic import BaseModel
from rapidy.http import post, Body, ContentType

class UserData(BaseModel):
    username: str
    password: str

@post('/')
async def handler(
    user_data: UserData = Body(),
    # or
    user_data: UserData = Body(content_type=ContentType.json),
    # or
    user_data: UserData = Body(content_type='application/json'),
) -> ...:

Отправка с помощью curl
curl -X POST \
-H "Content-Type: application/json" \
-d '{"username": "User", "password": "myAwesomePass"}' \
http://127.0.0.1:8080

Массив

[
    {"username": "User1", "password": "myAwesomePass1"},
    {"username": "User2", "password": "myAwesomePass2"}
]
from pydantic import BaseModel
from rapidy.http import post, Body

class UserData(BaseModel):
    username: str
    password: str

@post('/')
async def handler(
    users: list[UserData] = Body(),
) -> ...:

Отправка с помощью curl
curl -X POST \
-H "Content-Type: application/json" \
-d '[{"username": "user1", "password": "password1"}, {"username": "user2", "password": "password2"}]' \
http://127.0.0.1:8080

Число

Целое или вещественное значение.

111
from rapidy.http import post, Body

@post('/')
async def handler(
    int_data: int = Body(),
) -> ...:

Отправка с помощью curl
curl -X POST \
-H "Content-Type: application/json" \
-d '"111"' \
http://127.0.0.1:8080

При отправке строки в формате JSON требуется дополнительное экранирование символов: "111"

Литералы

true (логическое «истина»), false (логическое «ложь») и null (отсутствие значения).

true
false
null
from rapidy.http import post, Body

@post('/')
async def handler(
    bool_data: bool = Body(),
) -> ...:

Отправка с помощью curl

curl -X POST \
-H "Content-Type: application/json" \
-d 'true' \
http://127.0.0.1:8080
curl -X POST \
-H "Content-Type: application/json" \
-d 'false' \
http://127.0.0.1:8080
curl -X POST \
-H "Content-Type: application/json" \
-d 'null' \
http://127.0.0.1:8080

Строка

"SomeString"
from rapidy.http import post, Body

@post('/')
async def handler(
    string_data: str = Body(),
) -> ...:

Отправка с помощью curl
curl -X POST \
-H "Content-Type: application/json" \
-d '"SomeString"' \
http://127.0.0.1:8080

При отправке строки в формате JSON требуется экранирование символов: \"SomeString\"


Кастомный JSON-декодер

По умолчанию Rapidy использует json.loads без параметров для декодирования входящего JSON.

Эквивалентные примеры:

from rapidy.http import post, Body

@post('/')
async def handler(
    data: str = Body(),
) -> ...:
или
import json
from rapidy.http import post, Body

@post('/')
async def handler(
    data: str = Body(json_decoder=json.loads),
) -> ...:

Для использования кастомного декодера передайте вызываемый объект, принимающий str, в параметр json_decoder.

Ожидаемый тип: Callable[[str], Any]

Пример с пользовательским декодером:

from typing import Any
from rapidy.http import post, Body

def custom_json_decoder(data: str) -> ...:
    ...

@post('/')
async def handler(
    data: Any = Body(json_decoder=custom_json_decoder),
) -> ...:

Если необходимо использовать json.loads с параметрами, воспользуйтесь functools.partial:

import json
from functools import partial
from typing import Any, OrderedDict
from rapidy.http import post, Body

decoder = partial(json.loads, object_pairs_hook=OrderedDict)

@post('/')
async def handler(
    data: Any = Body(json_decoder=decoder),
) -> ...:


Извлечение без валидации

Отключение валидации не рекомендуется.

Если валидация отключена, параметр будет содержать данные в том виде, в котором их распаковал JSON-декодер.

Способы отключения валидации

Явное отключение

from pydantic import BaseModel
from rapidy.http import post, Body

class BodyData(BaseModel):
    ...

@post('/')
async def handler(
    data: BodyData = Body(validate=False),
) -> ...:

Использование Any

@post('/')
async def handler(
    data: Any = Body(),
) -> ...:

Отсутствие аннотации типа

@post('/')
async def handler(
    data=Body(),
) -> ...:

Значения по умолчанию

Если HTTP-запрос не содержит тела, параметр получит указанное значение по умолчанию (если оно задано).

Примеры использования

Указано значение по умолчанию

from pydantic import BaseModel
from rapidy.http import post, Body

class BodyData(BaseModel):
    ...

@post('/')
async def handler(
    data: BodyData = Body('some_data'),
    # or
    data: BodyData = Body(default_factory=lambda: 'some_data'),
) -> ...:

Опциональное тело запроса

from pydantic import BaseModel
from rapidy.http import post, Body

class BodyData(BaseModel):
    ...

@post('/')
async def handler(
    data: BodyData | None = Body(),
    # or
    data: Optional[BodyData] = Body(),
    # or
    data: Union[BodyData, None] = Body(),
) -> ...:

Извлечение сырых данных

Rapidy использует метод json объекта Request для получения данных и передает их в Pydantic для валидации.

Если данные не удастся извлечь как JSON, будет возвращена ошибка ExtractError:

{
    "errors": [
        {
            "type": "ExtractError",
            "loc": [
                "body"
            ],
            "msg": "Failed to extract body data as Json: <error_description>"
        }
    ]
}

Как происходит извлечение внутри Rapidy
async def extract_body_json(request: Request, body_field_info: Body) -> Optional[DictStrAny]:
    if not request.body_exists:
        return None

    return await request.json(loads=body_field_info.json_decoder)

Rapidy использует встроенные механизмы aiohttp

Подробнее об объекте aiohttp.Request и методах извлечения данных можно узнать здесь.

Однако если параметр аннотирован как bytes или StreamReader, данные извлекаются иначе.

Подробнее об объекте StreamReader можно узнать здесь.

bytes

from rapidy.http import post, Body

@post('/')
async def handler(
    user_data: bytes = Body(),
    # also you can use pydantic validation
    user_data: bytes = Body(min_length=1),
) -> ...:
Внутренний код Rapidy
async def extract_body_bytes(request: Request) -> Optional[bytes]:
    if not request.body_exists:
        return None

    return await request.read()

StreamReader

from rapidy import StreamReader
from rapidy.http import post, Body

@post('/')
async def handler(
    user_data: StreamReader = Body(),
) -> ...:
Внутренний код Rapidy
async def extract_body_stream(request: Request) -> Optional[StreamReader]:
    if not request.body_exists:
        return None

    return request.content

Валидация Pydantic для StreamReader не поддерживается.

Невозможно задать значение по умолчанию для StreamReader.

При попытке установить значение по умолчанию для Body с аннотацией StreamReader через default или default_factory будет поднята ошибка ParameterCannotUseDefaultError.

from rapidy import StreamReader
from rapidy.http import post, Body

@post('/')
async def handler(
    user_data: StreamReader = Body(default='SomeDefault'),
) -> ...:
------------------------------
Handler attribute with Type `Body` cannot have a default value.

Handler path: `<full_path>/main.py`
Handler name: `handler`
Attribute name: `data`
------------------------------