Skip to content

♻️ Fix JSON Schema for bytes, use "contentMediaType": "application/octet-stream" instead of "format": "binary"#14953

Merged
tiangolo merged 12 commits intomasterfrom
content-media-type
Feb 21, 2026
Merged

♻️ Fix JSON Schema for bytes, use "contentMediaType": "application/octet-stream" instead of "format": "binary"#14953
tiangolo merged 12 commits intomasterfrom
content-media-type

Conversation

@tiangolo
Copy link
Member

@tiangolo tiangolo commented Feb 21, 2026

♻️ 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.

@tiangolo tiangolo changed the title 🐛 Fix JSON Schema for files, use contentMediaType 🐛 Fix JSON Schema for files, use contentMediaType instead of format: binary Feb 21, 2026
@tiangolo tiangolo added the bug Something isn't working label Feb 21, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 21, 2026

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing content-media-type (089be9d) with master (cf05823)1

Open in CodSpeed

Footnotes

  1. No successful run was found on master (d2c17b6) during the generation of this report, so cf05823 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@YuriiMotov
Copy link
Member

YuriiMotov commented Feb 21, 2026

But it will now also change schema for other parameters (not only File parameters):

from typing import Annotated

from pydantic import BaseModel
from fastapi import FastAPI, Query

class Model(BaseModel):
    data: bytes

app = FastAPI()

@app.get("/")
def read_root(params: Annotated[Model, Query()]):
    pass

@app.post("/")
def post_root(params: Model):
    pass
See schema
{
  "openapi": "3.1.0",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "paths": {
    "/": {
      "get": {
        "summary": "Read Root",
        "operationId": "read_root__get",
        "parameters": [
          {
            "name": "data",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "contentMediaType": "application/octet-stream",  # <= contentMediaType for Query parameter
              "title": "Data"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Post Root",
        "operationId": "post_root__post",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Model"
              }
            }
          }
        },
        "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"
      },
      "Model": {
        "properties": {
          "data": {
            "type": "string",
            "contentMediaType": "application/octet-stream",  # <= contentMediaType for Json Body
            "title": "Data"
          }
        },
        "type": "object",
        "required": [
          "data"
        ],
        "title": "Model"
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array",
            "title": "Location"
          },
          "msg": {
            "type": "string",
            "title": "Message"
          },
          "type": {
            "type": "string",
            "title": "Error Type"
          },
          "input": {
            "title": "Input"
          },
          "ctx": {
            "type": "object",
            "title": "Context"
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError"
      }
    }
  }
}

Will it be correct?

@tiangolo
Copy link
Member Author

Ah, the thing is that using bytes in Query parameters is undefined/not supported, as a URL can't really have pure bytes, they have to be encoded in ASCII.

I also checked with Swagger UI and the behavior is the same with both "format": "binary" and "contentMediaType": "application/octet-stream". In both cases it renders the same and doesn't work.

image

@tiangolo tiangolo marked this pull request as ready for review February 21, 2026 07:58
@YuriiMotov
Copy link
Member

YuriiMotov commented Feb 21, 2026

Ah, the thing is that using bytes in Query parameters is undefined/not supported, as a URL can't really have pure bytes, they have to be encoded in ASCII.

I agree about Query parameters (and other non-Body parameters). But it also affects JSON Body parameters (see previous schema example).

Not sure how useful it is to use bytes in JSON Body models, but it might probably affect people who re-use existing models (defined somewhere else) to declare parameters

@tiangolo
Copy link
Member Author

Ah, yes, this would be the actual correct format. JSON also doesn't support direct bytes, it has to be strings encoded in UTF-8, it would have to be in base64 or similar.

So, if anyone is using bytes in other places, this would probably fix their JSON Schema, what they have probably doesn't work properly.

I'm also making a PR to Pydantic, just added here another part to support base64.

This would work:

from fastapi import FastAPI
from pydantic import BaseModel


class Model(BaseModel):
    data: bytes

    model_config = {"ser_json_bytes": "base64"}


app = FastAPI()


@app.post("/")
def post_root(params: Model):
    pass

And now it has the proper JSON Schema defininig it, and tools like Swagger UI can represent it correctly:

image

I also added all the references to the description in this PR.

@tiangolo tiangolo merged commit e8b98d2 into master Feb 21, 2026
39 checks passed
@tiangolo tiangolo deleted the content-media-type branch February 21, 2026 13:01
@tiangolo tiangolo changed the title 🐛 Fix JSON Schema for files, use contentMediaType instead of format: binary ♻️ Fix JSON Schema for bytes, use "contentMediaType": "application/octet-stream" instead of "format": "binary" Feb 21, 2026
@pperliti
Copy link

Hi @tiangolo,
from version 0.129.1 onward, parameters declared as a list of files (e.g., support_files: Annotated[List[UploadFile], File(description=f"Optional. Files used to extract data.")] = None) are rendered incorrectly in Swagger.

image

@OscarMCV-SW
Copy link

OscarMCV-SW commented Feb 27, 2026

Hi, same issue here. I rolled back my local FastAPI version and noticed that 0.129.1 breaks our schemas. When generating TS clients from openapi.json, some File arrays are now interpreted as arrays of strings.

It may be related to this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants