failed WRONG_FIX UNSUBMITTED wrong_fix_unsubmitted(None) · None tool calls · 0 s · fastapi/fastapi
♻️ Fix JSON Schema for bytes, use `"contentMediaType": "application/octet-stream"` instead of `"format": "binary"` ♻️ Fix JSON Schema for bytes, use `"contentMediaType": "application/octet-stream"` instead of `"format": "binary"` ## Background `format: binary` was defined in OpenAPI 3.0.x, in OpenAPI 3.1.x the schema was aligned with the latest JSON Schema, recommending instead `contentMediaType: application/octet-stream`. I suspect the JSON Schema for `bytes` using `"format": "binary"` comes from my first implementation in Pydantic 1.x. It was defined and suggested in OpenAPI 3.0.x (not in JSON Schema): https://spec.openapis.org/oas/v3.0.3.html#considerations-for-file-uploads OpenAPI 3.1.x aligned support with JSON Schema draft 07, so it was suggested to upate file uploads to use the regular JSON Schema format: `"contentMediaType": "application/octet-stream"`: https://learn.openapis.org/upgrading/v3.0-to-v3.1 This is defined in JSON Schema 07: https://json-schema.org/draft-07/json-schema-validation#rfc.section.8.4 ### JSON Schema 2020-12 Note Now OpenAPI 3.2 is aligned with JSON Schema 2020-12, which is what Pydantic v2 implements (except for this, I'm implementing it there too). It's the same as in JSON Schema draft 07, so this still applies: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#name-contentmediatype ### Usage in JSON JSON as a format actually doesn't support bytes, everything has to be in UTF-8 strings. Transporting bytes in JSON would require encoding bytes in a string, e.g. with base64. But as JSON Schema is not only defined to declare JSON payloads but also payloads that could have a comparable structure and defined with JSON Schema, it's still there in the spec.
| # | Tool | Arguments | Result |
|---|---|---|---|
| No trace captured. | |||
--- a/docs_src/json_base64_bytes/tutorial001_py310.py
+++ b/docs_src/json_base64_bytes/tutorial001_py310.py
@@ -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
--- a/fastapi/_compat/v2.py
+++ b/fastapi/_compat/v2.py
@@ -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
@@ -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": ...,
--- a/fastapi/datastructures.py
+++ b/fastapi/datastructures.py
@@ -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__(
--- a/scripts/playwright/json_base64_bytes/image01.py
+++ b/scripts/playwright/json_base64_bytes/image01.py
@@ -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()
==================================== ERRORS ====================================
______ ERROR collecting tests/test_request_params/test_file/test_list.py _______
tests/test_request_params/test_file/test_list.py:15: in <module>
@app.post("/list-bytes", operation_id="list_bytes")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
fastapi/routing.py:1125: in decorator
self.add_api_route(
fastapi/routing.py:1064: in add_api_route
route = route_class(
fastapi/routing.py:665: in __init__
self.dependant = get_dependant(
fastapi/dependencies/utils.py:279: in get_dependant
param_details = analyze_param(
fastapi/dependencies/utils.py:502: in analyze_param
ensure_multipart_is_installed()
fastapi/dependencies/utils.py:108: in ensure_multipart_is_installed
raise RuntimeError(multipart_not_installed_error) from None
E RuntimeError: Form data requires "python-multipart" to be installed.
E You can install "python-multipart" with:
E
E pip install python-multipart
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.52.1-py3-none-any/starlette/testclient.py:45
/Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.52.1-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 warning, 1 error in 0.68s