]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix bug preventing to use OpenAPI when using tuples (#3874)
authorVictor Benichoux <victor.benichoux@gmail.com>
Sun, 23 Jan 2022 22:13:55 +0000 (23:13 +0100)
committerGitHub <noreply@github.com>
Sun, 23 Jan 2022 22:13:55 +0000 (23:13 +0100)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
fastapi/openapi/models.py
tests/test_tuples.py [new file with mode: 0644]

index 361c7500547b0c315be56af68cf52593dfdcd6a1..9c6598d2d1a7b55c9bf17133f61bc046f03ad5cd 100644 (file)
@@ -123,7 +123,7 @@ class Schema(BaseModel):
     oneOf: Optional[List["Schema"]] = None
     anyOf: Optional[List["Schema"]] = None
     not_: Optional["Schema"] = Field(None, alias="not")
-    items: Optional["Schema"] = None
+    items: Optional[Union["Schema", List["Schema"]]] = None
     properties: Optional[Dict[str, "Schema"]] = None
     additionalProperties: Optional[Union["Schema", Reference, bool]] = None
     description: Optional[str] = None
diff --git a/tests/test_tuples.py b/tests/test_tuples.py
new file mode 100644 (file)
index 0000000..4cd5ee3
--- /dev/null
@@ -0,0 +1,267 @@
+from typing import List, Tuple
+
+from fastapi import FastAPI, Form
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class ItemGroup(BaseModel):
+    items: List[Tuple[str, str]]
+
+
+class Coordinate(BaseModel):
+    x: float
+    y: float
+
+
+@app.post("/model-with-tuple/")
+def post_model_with_tuple(item_group: ItemGroup):
+    return item_group
+
+
+@app.post("/tuple-of-models/")
+def post_tuple_of_models(square: Tuple[Coordinate, Coordinate]):
+    return square
+
+
+@app.post("/tuple-form/")
+def hello(values: Tuple[int, int] = Form(...)):
+    return values
+
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "FastAPI", "version": "0.1.0"},
+    "paths": {
+        "/model-with-tuple/": {
+            "post": {
+                "summary": "Post Model With Tuple",
+                "operationId": "post_model_with_tuple_model_with_tuple__post",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {"$ref": "#/components/schemas/ItemGroup"}
+                        }
+                    },
+                    "required": True,
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/tuple-of-models/": {
+            "post": {
+                "summary": "Post Tuple Of Models",
+                "operationId": "post_tuple_of_models_tuple_of_models__post",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "title": "Square",
+                                "maxItems": 2,
+                                "minItems": 2,
+                                "type": "array",
+                                "items": [
+                                    {"$ref": "#/components/schemas/Coordinate"},
+                                    {"$ref": "#/components/schemas/Coordinate"},
+                                ],
+                            }
+                        }
+                    },
+                    "required": True,
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+            }
+        },
+        "/tuple-form/": {
+            "post": {
+                "summary": "Hello",
+                "operationId": "hello_tuple_form__post",
+                "requestBody": {
+                    "content": {
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Body_hello_tuple_form__post"
+                            }
+                        }
+                    },
+                    "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_hello_tuple_form__post": {
+                "title": "Body_hello_tuple_form__post",
+                "required": ["values"],
+                "type": "object",
+                "properties": {
+                    "values": {
+                        "title": "Values",
+                        "maxItems": 2,
+                        "minItems": 2,
+                        "type": "array",
+                        "items": [{"type": "integer"}, {"type": "integer"}],
+                    }
+                },
+            },
+            "Coordinate": {
+                "title": "Coordinate",
+                "required": ["x", "y"],
+                "type": "object",
+                "properties": {
+                    "x": {"title": "X", "type": "number"},
+                    "y": {"title": "Y", "type": "number"},
+                },
+            },
+            "HTTPValidationError": {
+                "title": "HTTPValidationError",
+                "type": "object",
+                "properties": {
+                    "detail": {
+                        "title": "Detail",
+                        "type": "array",
+                        "items": {"$ref": "#/components/schemas/ValidationError"},
+                    }
+                },
+            },
+            "ItemGroup": {
+                "title": "ItemGroup",
+                "required": ["items"],
+                "type": "object",
+                "properties": {
+                    "items": {
+                        "title": "Items",
+                        "type": "array",
+                        "items": {
+                            "maxItems": 2,
+                            "minItems": 2,
+                            "type": "array",
+                            "items": [{"type": "string"}, {"type": "string"}],
+                        },
+                    }
+                },
+            },
+            "ValidationError": {
+                "title": "ValidationError",
+                "required": ["loc", "msg", "type"],
+                "type": "object",
+                "properties": {
+                    "loc": {
+                        "title": "Location",
+                        "type": "array",
+                        "items": {"type": "string"},
+                    },
+                    "msg": {"title": "Message", "type": "string"},
+                    "type": {"title": "Error Type", "type": "string"},
+                },
+            },
+        }
+    },
+}
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == openapi_schema
+
+
+def test_model_with_tuple_valid():
+    data = {"items": [["foo", "bar"], ["baz", "whatelse"]]}
+    response = client.post("/model-with-tuple/", json=data)
+    assert response.status_code == 200, response.text
+    assert response.json() == data
+
+
+def test_model_with_tuple_invalid():
+    data = {"items": [["foo", "bar"], ["baz", "whatelse", "too", "much"]]}
+    response = client.post("/model-with-tuple/", json=data)
+    assert response.status_code == 422, response.text
+
+    data = {"items": [["foo", "bar"], ["baz"]]}
+    response = client.post("/model-with-tuple/", json=data)
+    assert response.status_code == 422, response.text
+
+
+def test_tuple_with_model_valid():
+    data = [{"x": 1, "y": 2}, {"x": 3, "y": 4}]
+    response = client.post("/tuple-of-models/", json=data)
+    assert response.status_code == 200, response.text
+    assert response.json() == data
+
+
+def test_tuple_with_model_invalid():
+    data = [{"x": 1, "y": 2}, {"x": 3, "y": 4}, {"x": 5, "y": 6}]
+    response = client.post("/tuple-of-models/", json=data)
+    assert response.status_code == 422, response.text
+
+    data = [{"x": 1, "y": 2}]
+    response = client.post("/tuple-of-models/", json=data)
+    assert response.status_code == 422, response.text
+
+
+def test_tuple_form_valid():
+    response = client.post("/tuple-form/", data=[("values", "1"), ("values", "2")])
+    assert response.status_code == 200, response.text
+    assert response.json() == [1, 2]
+
+
+def test_tuple_form_invalid():
+    response = client.post(
+        "/tuple-form/", data=[("values", "1"), ("values", "2"), ("values", "3")]
+    )
+    assert response.status_code == 422, response.text
+
+    response = client.post("/tuple-form/", data=[("values", "1")])
+    assert response.status_code == 422, response.text