Skip to content

Commit 058220c

Browse files
committed
fix: add OpenAPI schema support for UploadFile class
1 parent c1f72f7 commit 058220c

File tree

3 files changed

+183
-1
lines changed

3 files changed

+183
-1
lines changed

aws_lambda_powertools/event_handler/openapi/params.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,19 @@ def __get_pydantic_core_schema__(
115115
from pydantic_core import core_schema
116116

117117
# Define the schema for UploadFile validation
118-
return core_schema.no_info_plain_validator_function(
118+
schema = core_schema.no_info_plain_validator_function(
119119
cls._validate,
120120
serialization=core_schema.to_string_ser_schema(),
121121
)
122+
123+
# Add OpenAPI schema info
124+
schema["json_schema_extra"] = {
125+
"type": "string",
126+
"format": "binary",
127+
"description": "A file uploaded as part of a multipart/form-data request",
128+
}
129+
130+
return schema
122131

123132
@classmethod
124133
def _validate(cls, value: Any) -> UploadFile:
@@ -128,6 +137,28 @@ def _validate(cls, value: Any) -> UploadFile:
128137
if isinstance(value, bytes):
129138
return cls(file=value)
130139
raise ValueError(f"Expected UploadFile or bytes, got {type(value)}")
140+
141+
@classmethod
142+
def __get_validators__(cls):
143+
"""Return validators for Pydantic v1 compatibility."""
144+
yield cls._validate
145+
146+
@classmethod
147+
def __get_pydantic_json_schema__(
148+
cls, _core_schema: Any, field_schema: Any
149+
) -> dict[str, Any]:
150+
"""Modify the JSON schema for OpenAPI compatibility."""
151+
# Handle both Pydantic v1 and v2 schemas
152+
json_schema = field_schema(_core_schema) if callable(field_schema) else {}
153+
154+
# Add binary file format for OpenAPI
155+
json_schema.update(
156+
type="string",
157+
format="binary",
158+
description="A file uploaded as part of a multipart/form-data request",
159+
)
160+
161+
return json_schema
131162

132163

133164
class ParamTypes(Enum):
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Example of using UploadFile with OpenAPI schema generation
3+
4+
This example demonstrates how to use the UploadFile class with FastAPI-like
5+
file handling and proper OpenAPI schema generation.
6+
"""
7+
8+
from typing_extensions import Annotated, List
9+
10+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
11+
from aws_lambda_powertools.event_handler.openapi.params import File, Form, UploadFile
12+
13+
app = APIGatewayRestResolver()
14+
15+
16+
@app.post("/upload")
17+
def upload_file(file: Annotated[UploadFile, File()]):
18+
"""
19+
Upload a single file.
20+
21+
Returns file metadata and a preview of the content.
22+
"""
23+
return {
24+
"filename": file.filename,
25+
"content_type": file.content_type,
26+
"size": file.size,
27+
"content_preview": file.file[:100].decode() if file.size < 10000 else "Content too large to preview",
28+
}
29+
30+
31+
@app.post("/upload-multiple")
32+
def upload_multiple_files(
33+
primary_file: Annotated[UploadFile, File(alias="primary", description="Primary file with metadata")],
34+
secondary_file: Annotated[bytes, File(alias="secondary", description="Secondary file as bytes")],
35+
description: Annotated[str, Form(description="Description of the uploaded files")],
36+
):
37+
"""
38+
Upload multiple files with form data.
39+
40+
Shows how to mix UploadFile, bytes files, and form data in the same endpoint.
41+
"""
42+
return {
43+
"status": "uploaded",
44+
"description": description,
45+
"primary_filename": primary_file.filename,
46+
"primary_content_type": primary_file.content_type,
47+
"primary_size": primary_file.size,
48+
"secondary_size": len(secondary_file),
49+
"total_size": primary_file.size + len(secondary_file),
50+
}
51+
52+
53+
@app.post("/upload-with-headers")
54+
def upload_with_headers(file: Annotated[UploadFile, File()]):
55+
"""
56+
Upload a file and access its headers.
57+
58+
Demonstrates how to access all headers from the multipart section.
59+
"""
60+
return {
61+
"filename": file.filename,
62+
"content_type": file.content_type,
63+
"size": file.size,
64+
"headers": file.headers,
65+
}
66+
67+
68+
def handler(event, context):
69+
return app.resolve(event, context)
70+
71+
72+
if __name__ == "__main__":
73+
# Print the OpenAPI schema for testing
74+
schema = app.get_openapi_schema(title="File Upload API", version="1.0.0")
75+
print("\n✅ OpenAPI schema generated successfully!")
76+
77+
# You can access the schema as JSON with:
78+
# import json
79+
# print(json.dumps(schema.model_dump(), indent=2))
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import pytest
2+
import json
3+
from typing_extensions import Annotated
4+
5+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
6+
from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile
7+
8+
9+
class TestUploadFileOpenAPISchema:
10+
"""Test UploadFile OpenAPI schema generation."""
11+
12+
def test_upload_file_openapi_schema(self):
13+
"""Test OpenAPI schema generation with UploadFile."""
14+
app = APIGatewayRestResolver()
15+
16+
@app.post("/upload-single")
17+
def upload_single_file(file: Annotated[UploadFile, File()]):
18+
"""Upload a single file."""
19+
return {"filename": file.filename, "size": file.size}
20+
21+
@app.post("/upload-multiple")
22+
def upload_multiple_files(
23+
primary_file: Annotated[UploadFile, File(alias="primary", description="Primary file with metadata")],
24+
secondary_file: Annotated[bytes, File(alias="secondary", description="Secondary file as bytes")],
25+
):
26+
"""Upload multiple files - showcasing BOTH UploadFile and bytes approaches."""
27+
return {
28+
"status": "uploaded",
29+
"primary_filename": primary_file.filename,
30+
"primary_content_type": primary_file.content_type,
31+
"primary_size": primary_file.size,
32+
"secondary_size": len(secondary_file),
33+
"total_size": primary_file.size + len(secondary_file),
34+
}
35+
36+
# Generate OpenAPI schema
37+
schema = app.get_openapi_schema()
38+
39+
# Print schema for debugging
40+
schema_dict = schema.model_dump()
41+
print("SCHEMA PATHS:")
42+
for path, path_item in schema_dict["paths"].items():
43+
print(f"Path: {path}")
44+
if "post" in path_item:
45+
if "requestBody" in path_item["post"]:
46+
if "content" in path_item["post"]["requestBody"]:
47+
if "multipart/form-data" in path_item["post"]["requestBody"]["content"]:
48+
print(" Found multipart/form-data")
49+
print(f" Schema: {json.dumps(path_item['post']['requestBody']['content']['multipart/form-data'], indent=2)}")
50+
51+
print("\nSCHEMA COMPONENTS:")
52+
if "components" in schema_dict and "schemas" in schema_dict["components"]:
53+
for name, comp_schema in schema_dict["components"]["schemas"].items():
54+
if "file" in name.lower() or "upload" in name.lower():
55+
print(f"Component: {name}")
56+
print(f" {json.dumps(comp_schema, indent=2)}")
57+
58+
# Basic verification
59+
paths = schema.paths
60+
assert "/upload-single" in paths
61+
assert "/upload-multiple" in paths
62+
63+
# Verify upload-single endpoint exists
64+
upload_single = paths["/upload-single"]
65+
assert upload_single.post is not None
66+
67+
# Verify upload-multiple endpoint exists
68+
upload_multiple = paths["/upload-multiple"]
69+
assert upload_multiple.post is not None
70+
71+
# Print success
72+
print("\n✅ Basic OpenAPI schema generation tests passed")

0 commit comments

Comments
 (0)