Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Make sure a parameter defined as required is kept required in OpenAPI even if defined as optional in another dependency #4319

Merged
merged 8 commits into from Sep 3, 2022
15 changes: 13 additions & 2 deletions fastapi/openapi/utils.py
Expand Up @@ -166,6 +166,17 @@ def get_openapi_operation_metadata(
return operation


def get_openapi_operation_request_parameters(
*, parameters: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
operation_request_parameters: Dict[str, Dict[str, Any]] = {}
for param in parameters:
if param["name"] not in operation_request_parameters or param["required"]:
operation_request_parameters[param["name"]] = param

return list(operation_request_parameters.values())


def get_openapi_path(
*, route: routing.APIRoute, model_name_map: Dict[type, str]
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
Expand Down Expand Up @@ -197,8 +208,8 @@ def get_openapi_path(
)
parameters.extend(operation_parameters)
if parameters:
operation["parameters"] = list(
{param["name"]: param for param in parameters}.values()
operation["parameters"] = get_openapi_operation_request_parameters(
parameters=parameters
)
if method in METHODS_WITH_BODY:
request_body_oai = get_openapi_operation_request_body(
Expand Down
105 changes: 105 additions & 0 deletions tests/test_enforce_once_required_parameter.py
@@ -0,0 +1,105 @@
from typing import Optional

from fastapi import Depends, FastAPI, Query, status
from fastapi.testclient import TestClient

app = FastAPI()


def _get_client_key(client_id: str = Query(...)) -> str:
return f"{client_id}_key"


def _get_client_tag(client_id: Optional[str] = Query(None)) -> Optional[str]:
if client_id is None:
return None
return f"{client_id}_tag"


@app.get("/foo")
def foo_handler(
client_key: str = Depends(_get_client_key),
client_tag: Optional[str] = Depends(_get_client_tag),
):
return {"client_id": client_key, "client_tag": client_tag}


client = TestClient(app)

expected_schema = {
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {"$ref": "#/components/schemas/ValidationError"},
"title": "Detail",
"type": "array",
}
},
"title": "HTTPValidationError",
"type": "object",
},
"ValidationError": {
"properties": {
"loc": {
"items": {"type": "string"},
"title": "Location",
"type": "array",
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error " "Type", "type": "string"},
},
"required": ["loc", "msg", "type"],
"title": "ValidationError",
"type": "object",
},
}
},
"info": {"title": "FastAPI", "version": "0.1.0"},
"openapi": "3.0.2",
"paths": {
"/foo": {
"get": {
"operationId": "foo_handler_foo_get",
"parameters": [
{
"in": "query",
"name": "client_id",
"required": True,
"schema": {"title": "Client Id", "type": "string"},
},
],
"responses": {
"200": {
"content": {"application/json": {"schema": {}}},
"description": "Successful " "Response",
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation " "Error",
},
},
"summary": "Foo Handler",
}
}
},
}


def test_schema():
response = client.get("/openapi.json")
assert response.status_code == status.HTTP_200_OK
actual_schema = response.json()
assert actual_schema == expected_schema


def test_get_foo():
response = client.get("/foo", params={"client_id": None})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY