Skip to content

🦖 Fix multiple upload file schema to be compatible with OAS 3.0 to make it work in Swagger UI#15069

Open
YuriiMotov wants to merge 5 commits into
masterfrom
fix-multiple-upload-file-in-swagger
Open

🦖 Fix multiple upload file schema to be compatible with OAS 3.0 to make it work in Swagger UI#15069
YuriiMotov wants to merge 5 commits into
masterfrom
fix-multiple-upload-file-in-swagger

Conversation

@YuriiMotov

@YuriiMotov YuriiMotov commented Mar 6, 2026

Copy link
Copy Markdown
Member

#14953 updated schema of byte fields to be in line with OAS 3.1.
But Swagger UI doesn't seem to fully support it and we have text fields instead of file pickers for multiple file upload fields (see #14975).

The idea of this PR is to make the schema of file upload fields backward compatible with OAS 3.0 (which is handled well by Swagger UI) by adding "format": "binary":

{
      "type": "string",
+     "format": "binary",
      "contentMediaType": "application/octet-stream",
}

OAS 3.1+ will just ignore it (see spec) and OAS 3.0 - compatible tools will just ignore contentMediaType (as it's not defined in OAS 3.0).

We have to handle separately situation when bytes are inside JSON (not file upload). To do this we need to somehow pass context (whether it's a File field or not) to bytes_schema.
Cloude Code suggested to do it by adding _FileUploadMarker marker. I reviewed and it looks good to me (after several iterations :))


Tested manually with the following app:

Details
from typing import Annotated

from fastapi import FastAPI, File, UploadFile
from pydantic import BaseModel

app = FastAPI()


# ---

@app.post("/upload-files-bytes")
async def upload_files_bytes(p: Annotated[list[bytes], File()]):
    return {"file_sizes": [len(file) for file in p] if p else None}


@app.post("/upload-files-uploadfile")
async def upload_files_uploadfile(p: Annotated[list[UploadFile], File()]):
    return {"file_sizes": [file.size for file in p] if p else None}


@app.post("/upload-file-bytes")
async def upload_file_bytes(p: Annotated[bytes, File()]):
    return {"file_sizes": len(p) if p else None}


@app.post("/upload-file-uploadfile")
async def upload_file_uploadfile(p: Annotated[UploadFile, File()]):
    return {"file_sizes": p.size if p else None}


# ---


@app.post("/opt-upload-files-bytes")
async def opt_upload_files_bytes(p: Annotated[list[bytes] | None, File()] = None):
    return {"file_sizes": [len(file) for file in p] if p else None}


@app.post("/opt-upload-files-uploadfile")
async def opt_upload_files_uploadfile(p: Annotated[list[UploadFile] | None, File()] = None):
    return {"file_sizes": [file.size for file in p] if p else None}


@app.post("/opt-upload-file-bytes")
async def opt_upload_file_bytes(p: Annotated[bytes | None, File()] = None):
    return {"file_sizes": len(p) if p else None}


@app.post("/opt-upload-file-uploadfile")
async def opt_upload_file_uploadfile(p: Annotated[UploadFile | None, File()] = None):
    return {"file_sizes": p.size if p else None}


# ---

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

class DataInputBase64(DataInput):
    model_config = {"val_json_bytes": "base64"}


@app.post("/data")
def post_data(body: DataInput):  # Will NOT decode base64 (will return as is)
    content = body.data.decode("utf-8")
    return {"description": body.description, "content": content}


@app.post("/data-base64")
def post_data_base64(body: DataInputBase64):  # Will decode base64
    content = body.data.decode("utf-8")
    return {"description": body.description, "content": content}

Produced OpenAPI schema:

Details
{
  "openapi": "3.1.0",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "paths": {
    "/upload-files-bytes": {
      "post": {
        "summary": "Upload Files Bytes",
        "operationId": "upload_files_bytes_upload_files_bytes_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_upload_files_bytes_upload_files_bytes_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/upload-files-uploadfile": {
      "post": {
        "summary": "Upload Files Uploadfile",
        "operationId": "upload_files_uploadfile_upload_files_uploadfile_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_upload_files_uploadfile_upload_files_uploadfile_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/upload-file-bytes": {
      "post": {
        "summary": "Upload File Bytes",
        "operationId": "upload_file_bytes_upload_file_bytes_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_upload_file_bytes_upload_file_bytes_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/upload-file-uploadfile": {
      "post": {
        "summary": "Upload File Uploadfile",
        "operationId": "upload_file_uploadfile_upload_file_uploadfile_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_upload_file_uploadfile_upload_file_uploadfile_post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/opt-upload-files-bytes": {
      "post": {
        "summary": "Opt Upload Files Bytes",
        "operationId": "opt_upload_files_bytes_opt_upload_files_bytes_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_opt_upload_files_bytes_opt_upload_files_bytes_post"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/opt-upload-files-uploadfile": {
      "post": {
        "summary": "Opt Upload Files Uploadfile",
        "operationId": "opt_upload_files_uploadfile_opt_upload_files_uploadfile_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_opt_upload_files_uploadfile_opt_upload_files_uploadfile_post"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/opt-upload-file-bytes": {
      "post": {
        "summary": "Opt Upload File Bytes",
        "operationId": "opt_upload_file_bytes_opt_upload_file_bytes_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_opt_upload_file_bytes_opt_upload_file_bytes_post"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/opt-upload-file-uploadfile": {
      "post": {
        "summary": "Opt Upload File Uploadfile",
        "operationId": "opt_upload_file_uploadfile_opt_upload_file_uploadfile_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_opt_upload_file_uploadfile_opt_upload_file_uploadfile_post"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/data": {
      "post": {
        "summary": "Post Data",
        "operationId": "post_data_data_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DataInput"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/data-base64": {
      "post": {
        "summary": "Post Data Base64",
        "operationId": "post_data_base64_data_base64_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DataInputBase64"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Body_opt_upload_file_bytes_opt_upload_file_bytes_post": {
        "properties": {
          "p": {
            "anyOf": [
              {
                "type": "string",
                "format": "binary",  <= Added
                "contentMediaType": "application/octet-stream"
              },
              {
                "type": "null"
              }
            ],
            "title": "P"
          }
        },
        "type": "object",
        "title": "Body_opt_upload_file_bytes_opt_upload_file_bytes_post"
      },
      "Body_opt_upload_file_uploadfile_opt_upload_file_uploadfile_post": {
        "properties": {
          "p": {
            "anyOf": [
              {
                "type": "string",
                "format": "binary",  <= Added
                "contentMediaType": "application/octet-stream"
              },
              {
                "type": "null"
              }
            ],
            "title": "P"
          }
        },
        "type": "object",
        "title": "Body_opt_upload_file_uploadfile_opt_upload_file_uploadfile_post"
      },
      "Body_opt_upload_files_bytes_opt_upload_files_bytes_post": {
        "properties": {
          "p": {
            "anyOf": [
              {
                "items": {
                  "type": "string",
                  "format": "binary",  <= Added
                  "contentMediaType": "application/octet-stream"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "P"
          }
        },
        "type": "object",
        "title": "Body_opt_upload_files_bytes_opt_upload_files_bytes_post"
      },
      "Body_opt_upload_files_uploadfile_opt_upload_files_uploadfile_post": {
        "properties": {
          "p": {
            "anyOf": [
              {
                "items": {
                  "type": "string",
                  "format": "binary",  <= Added
                  "contentMediaType": "application/octet-stream"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "P"
          }
        },
        "type": "object",
        "title": "Body_opt_upload_files_uploadfile_opt_upload_files_uploadfile_post"
      },
      "Body_upload_file_bytes_upload_file_bytes_post": {
        "properties": {
          "p": {
            "type": "string",
            "format": "binary",  <= Added
            "contentMediaType": "application/octet-stream",
            "title": "P"
          }
        },
        "type": "object",
        "required": [
          "p"
        ],
        "title": "Body_upload_file_bytes_upload_file_bytes_post"
      },
      "Body_upload_file_uploadfile_upload_file_uploadfile_post": {
        "properties": {
          "p": {
            "type": "string",
            "format": "binary",  <= Added
            "contentMediaType": "application/octet-stream",
            "title": "P"
          }
        },
        "type": "object",
        "required": [
          "p"
        ],
        "title": "Body_upload_file_uploadfile_upload_file_uploadfile_post"
      },
      "Body_upload_files_bytes_upload_files_bytes_post": {
        "properties": {
          "p": {
            "items": {
              "type": "string",
              "format": "binary",  <= Added
              "contentMediaType": "application/octet-stream"
            },
            "type": "array",
            "title": "P"
          }
        },
        "type": "object",
        "required": [
          "p"
        ],
        "title": "Body_upload_files_bytes_upload_files_bytes_post"
      },
      "Body_upload_files_uploadfile_upload_files_uploadfile_post": {
        "properties": {
          "p": {
            "items": {
              "type": "string",
              "format": "binary",  <= Added
              "contentMediaType": "application/octet-stream"
            },
            "type": "array",
            "title": "P"
          }
        },
        "type": "object",
        "required": [
          "p"
        ],
        "title": "Body_upload_files_uploadfile_upload_files_uploadfile_post"
      },
      "DataInput": {
        "properties": {
          "description": {
            "type": "string",
            "title": "Description"
          },
          "data": {
            "type": "string",  <= No 'format: binary' for JSON
            "contentMediaType": "application/octet-stream",
            "title": "Data"
          }
        },
        "type": "object",
        "required": [
          "description",
          "data"
        ],
        "title": "DataInput"
      },
      "DataInputBase64": {
        "properties": {
          "description": {
            "type": "string",
            "title": "Description"
          },
          "data": {
            "type": "string",  <= No 'format: binary' for JSON
            "contentEncoding": "base64",
            "contentMediaType": "application/octet-stream",
            "title": "Data"
          }
        },
        "type": "object",
        "required": [
          "description",
          "data"
        ],
        "title": "DataInputBase64"
      },
      "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"
          },
          "input": {
            "title": "Input"
          },
          "ctx": {
            "type": "object",
            "title": "Context"
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError"
      }
    }
  }
}

All endpoints work in Swagger UI

@YuriiMotov YuriiMotov added the bug Something isn't working label Mar 6, 2026
@codspeed-hq

codspeed-hq Bot commented Mar 6, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing fix-multiple-upload-file-in-swagger (e5dbca9) with master (3969ae8)1

Open in CodSpeed

Footnotes

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

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has a merge conflict that needs to be resolved.

@github-actions github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label Mar 15, 2026
@github-actions github-actions Bot removed the conflicts Automatically generated when a PR has a merge conflict label Mar 16, 2026
@tiangolo

Copy link
Copy Markdown
Member

I was thinking we could just include the "format": "binary" the same way as before, have both there, I would hope that would simplify the logic. What do you think?

@YuriiMotov

YuriiMotov commented Mar 23, 2026

Copy link
Copy Markdown
Member Author

I was thinking we could just include the "format": "binary" the same way as before, have both there, I would hope that would simplify the logic. What do you think?

My bad, I didn't describe properly why we need to handle bytes inside JSON differently..

Specifying format: binary for bytes field inside JSON would be incorrect. In this case we should use format: byte (but we don't actually need to add it, so I omitted it).
See: https://spec.openapis.org/oas/v3.0.4.html#working-with-binary-data

@kbumsik

kbumsik commented Apr 13, 2026

Copy link
Copy Markdown

Hi, will it be reviewed soon? I would like to contribute to this PR if more work is needed.

@github-actions github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label May 11, 2026
@github-actions

This comment was marked as resolved.

@brainbytes42

Copy link
Copy Markdown

I ran into this issue having multiple files in my upload schema, but the docs having weird text input boxes. If I understand correctly, this PR should resolve it, but seems to be stale? Any chance to fix this issue, as the docs with ability to test the api are really great, if it works. Thanks! :-)

@YuriiMotov

Copy link
Copy Markdown
Member Author

While we are waiting for the issue to be fixed, you can use workaround described here: #14975 (comment)

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