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
63 changes: 63 additions & 0 deletions docs/en/docs/advanced/json-base64-bytes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# JSON with Bytes as Base64 { #json-with-bytes-as-base64 }

If your app needs to receive and send JSON data, but you need to include binary data in it, you can encode it as base64.

## Base64 vs Files { #base64-vs-files }

Consider first if you can use [Request Files](../tutorial/request-files.md){.internal-link target=_blank} for uploading binary data and [Custom Response - FileResponse](./custom-response.md#fileresponse--fileresponse-){.internal-link target=_blank} for sending binary data, instead of encoding it in JSON.

JSON can only contain UTF-8 encoded strings, so it can't contain raw bytes.

Base64 can encode binary data in strings, but to do it, it needs to use more characters than the original binary data, so it would normally be less efficient than regular files.

Use base64 only if you definitely need to include binary data in JSON, and you can't use files for that.

## Pydantic `bytes` { #pydantic-bytes }

You can declare a Pydantic model with `bytes` fields, and then use `val_json_bytes` in the model config to tell it to use base64 to *validate* input JSON data, as part of that validation it will decode the base64 string into bytes.

{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:9,29:35] hl[9] *}

If you check the `/docs`, they will show that the field `data` expects base64 encoded bytes:

<div class="screenshot">
<img src="/img/tutorial/json-base64-bytes/image01.png">
</div>

You could send a request like:

```json
{
"description": "Some data",
"data": "aGVsbG8="
}
```

/// tip

`aGVsbG8=` is the base64 encoding of `hello`.

///

And then Pydantic will decode the base64 string and give you the original bytes in the `data` field of the model.

You will receive a response like:

```json
{
"description": "Some data",
"content": "hello"
}
```

## Pydantic `bytes` for Output Data { #pydantic-bytes-for-output-data }

You can also use `bytes` fields with `ser_json_bytes` in the model config for output data, and Pydantic will *serialize* the bytes as base64 when generating the JSON response.

{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,12:16,29,38:41] hl[16] *}

## Pydantic `bytes` for Input and Output Data { #pydantic-bytes-for-input-and-output-data }

And of course, you can use the same model configured to use base64 to handle both input (*validate*) with `val_json_bytes` and output (*serialize*) with `ser_json_bytes` when receiving and sending JSON data.

{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,19:26,29,44:46] hl[23:26] *}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/en/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ nav:
- advanced/wsgi.md
- advanced/generate-clients.md
- advanced/advanced-python-types.md
- advanced/json-base64-bytes.md
- fastapi-cli.md
- Deployment:
- deployment/index.md
Expand Down
Empty file.
46 changes: 46 additions & 0 deletions docs_src/json_base64_bytes/tutorial001_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from fastapi import FastAPI
from pydantic import BaseModel


class DataInput(BaseModel):
description: str
data: bytes

model_config = {"val_json_bytes": "base64"}


class DataOutput(BaseModel):
description: str
data: bytes

model_config = {"ser_json_bytes": "base64"}


class DataInputOutput(BaseModel):
description: str
data: bytes

model_config = {
"val_json_bytes": "base64",
"ser_json_bytes": "base64",
}


app = FastAPI()


@app.post("/data")
def post_data(body: DataInput):
content = body.data.decode("utf-8")
return {"description": body.description, "content": content}


@app.get("/data")
def get_data() -> DataOutput:
data = "hello".encode("utf-8")
return DataOutput(description="A plumbus", data=data)


@app.post("/data-in-out")
def post_data_in_out(body: DataInputOutput) -> DataInputOutput:
return body
19 changes: 18 additions & 1 deletion fastapi/_compat/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
)
from pydantic._internal._typing_extra import eval_type_lenient
from pydantic.fields import FieldInfo as FieldInfo
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
from pydantic.json_schema import GenerateJsonSchema as _GenerateJsonSchema
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
from pydantic_core import CoreSchema as CoreSchema
from pydantic_core import PydanticUndefined
Expand All @@ -40,6 +40,23 @@
Undefined = PydanticUndefined
evaluate_forwardref = eval_type_lenient


class GenerateJsonSchema(_GenerateJsonSchema):
# TODO: remove when this is merged (or equivalent): https://github.com/pydantic/pydantic/pull/12841
# and dropping support for any version of Pydantic before that one (so, in a very long time)
def bytes_schema(self, schema: CoreSchema) -> JsonSchemaValue:
json_schema = {"type": "string", "contentMediaType": "application/octet-stream"}
bytes_mode = (
self._config.ser_json_bytes
if self.mode == "serialization"
else self._config.val_json_bytes
)
if bytes_mode == "base64":
json_schema["contentEncoding"] = "base64"
self.update_with_validations(json_schema, schema, self.ValidationsMapping.bytes)
return json_schema


# TODO: remove when dropping support for Pydantic < v2.12.3
_Attrs = {
"default": ...,
Expand Down
2 changes: 1 addition & 1 deletion fastapi/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
def __get_pydantic_json_schema__(
cls, core_schema: Mapping[str, Any], handler: GetJsonSchemaHandler
) -> dict[str, Any]:
return {"type": "string", "format": "binary"}
return {"type": "string", "contentMediaType": "application/octet-stream"}

@classmethod
def __get_pydantic_core_schema__(
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ ignore = [
"docs_src/security/tutorial005_an_py39.py" = ["B904"]
"docs_src/security/tutorial005_py310.py" = ["B904"]
"docs_src/security/tutorial005_py39.py" = ["B904"]
"docs_src/json_base64_bytes/tutorial001_py310.py" = ["UP012"]

[tool.ruff.lint.isort]
known-third-party = ["fastapi", "pydantic", "starlette"]
Expand Down
37 changes: 37 additions & 0 deletions scripts/playwright/json_base64_bytes/image01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import subprocess
import time

import httpx
from playwright.sync_api import Playwright, sync_playwright


# Run playwright codegen to generate the code below, copy paste the sections in run()
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False)
# Update the viewport manually
context = browser.new_context(viewport={"width": 960, "height": 1080})
page = context.new_page()
page.goto("http://localhost:8000/docs")
page.get_by_role("button", name="POST /data Post Data").click()
# Manually add the screenshot
page.screenshot(path="docs/en/docs/img/tutorial/json-base64-bytes/image01.png")

# ---------------------
context.close()
browser.close()


process = subprocess.Popen(
["fastapi", "run", "docs_src/json_base64_bytes/tutorial001_py310.py"]
)
try:
for _ in range(3):
try:
response = httpx.get("http://localhost:8000/docs")
except httpx.ConnectError:
time.sleep(1)
break
with sync_playwright() as playwright:
run(playwright)
finally:
process.terminate()
20 changes: 16 additions & 4 deletions tests/test_request_params/test_file/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ def test_list_schema(path: str):
"properties": {
"p": {
"type": "array",
"items": {"type": "string", "format": "binary"},
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"title": "P",
},
},
Expand Down Expand Up @@ -115,7 +118,10 @@ def test_list_alias_schema(path: str):
"properties": {
"p_alias": {
"type": "array",
"items": {"type": "string", "format": "binary"},
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"title": "P Alias",
},
},
Expand Down Expand Up @@ -221,7 +227,10 @@ def test_list_validation_alias_schema(path: str):
"properties": {
"p_val_alias": {
"type": "array",
"items": {"type": "string", "format": "binary"},
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"title": "P Val Alias",
},
},
Expand Down Expand Up @@ -338,7 +347,10 @@ def test_list_alias_and_validation_alias_schema(path: str):
"properties": {
"p_val_alias": {
"type": "array",
"items": {"type": "string", "format": "binary"},
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
"title": "P Val Alias",
},
},
Expand Down
8 changes: 4 additions & 4 deletions tests/test_request_params/test_file/test_optional.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def test_optional_schema(path: str):
"properties": {
"p": {
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "string", "contentMediaType": "application/octet-stream"},
{"type": "null"},
],
"title": "P",
Expand Down Expand Up @@ -109,7 +109,7 @@ def test_optional_alias_schema(path: str):
"properties": {
"p_alias": {
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "string", "contentMediaType": "application/octet-stream"},
{"type": "null"},
],
"title": "P Alias",
Expand Down Expand Up @@ -200,7 +200,7 @@ def test_optional_validation_alias_schema(path: str):
"properties": {
"p_val_alias": {
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "string", "contentMediaType": "application/octet-stream"},
{"type": "null"},
],
"title": "P Val Alias",
Expand Down Expand Up @@ -296,7 +296,7 @@ def test_optional_alias_and_validation_alias_schema(path: str):
"properties": {
"p_val_alias": {
"anyOf": [
{"type": "string", "format": "binary"},
{"type": "string", "contentMediaType": "application/octet-stream"},
{"type": "null"},
],
"title": "P Val Alias",
Expand Down
20 changes: 16 additions & 4 deletions tests/test_request_params/test_file/test_optional_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ def test_optional_list_schema(path: str):
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
},
{"type": "null"},
],
Expand Down Expand Up @@ -116,7 +119,10 @@ def test_optional_list_alias_schema(path: str):
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
},
{"type": "null"},
],
Expand Down Expand Up @@ -205,7 +211,10 @@ def test_optional_validation_alias_schema(path: str):
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
},
{"type": "null"},
],
Expand Down Expand Up @@ -301,7 +310,10 @@ def test_optional_list_alias_and_validation_alias_schema(path: str):
"anyOf": [
{
"type": "array",
"items": {"type": "string", "format": "binary"},
"items": {
"type": "string",
"contentMediaType": "application/octet-stream",
},
},
{"type": "null"},
],
Expand Down
16 changes: 12 additions & 4 deletions tests/test_request_params/test_file/test_required.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ def test_required_schema(path: str):

assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p": {"title": "P", "type": "string", "format": "binary"},
"p": {
"title": "P",
"type": "string",
"contentMediaType": "application/octet-stream",
},
},
"required": ["p"],
"title": body_model_name,
Expand Down Expand Up @@ -109,7 +113,11 @@ def test_required_alias_schema(path: str):

assert app.openapi()["components"]["schemas"][body_model_name] == {
"properties": {
"p_alias": {"title": "P Alias", "type": "string", "format": "binary"},
"p_alias": {
"title": "P Alias",
"type": "string",
"contentMediaType": "application/octet-stream",
},
},
"required": ["p_alias"],
"title": body_model_name,
Expand Down Expand Up @@ -216,7 +224,7 @@ def test_required_validation_alias_schema(path: str):
"p_val_alias": {
"title": "P Val Alias",
"type": "string",
"format": "binary",
"contentMediaType": "application/octet-stream",
},
},
"required": ["p_val_alias"],
Expand Down Expand Up @@ -329,7 +337,7 @@ def test_required_alias_and_validation_alias_schema(path: str):
"p_val_alias": {
"title": "P Val Alias",
"type": "string",
"format": "binary",
"contentMediaType": "application/octet-stream",
},
},
"required": ["p_val_alias"],
Expand Down
Empty file.
Loading