From dc1e94d05f92d7b75f398557ce438b0c56ebcf8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 25 Mar 2019 23:28:09 +0400 Subject: [PATCH] :sparkles: Document and test union and list response models (#108) --- docs/src/extra_models/tutorial003.py | 35 +++++ docs/src/extra_models/tutorial004.py | 22 +++ docs/tutorial/extra-models.md | 22 +++ .../test_extra_models/__init__.py | 0 .../test_extra_models/test_tutorial003.py | 125 ++++++++++++++++++ .../test_extra_models/test_tutorial004.py | 60 +++++++++ 6 files changed, 264 insertions(+) create mode 100644 docs/src/extra_models/tutorial003.py create mode 100644 docs/src/extra_models/tutorial004.py create mode 100644 tests/test_tutorial/test_extra_models/__init__.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial003.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial004.py diff --git a/docs/src/extra_models/tutorial003.py b/docs/src/extra_models/tutorial003.py new file mode 100644 index 000000000..065439acc --- /dev/null +++ b/docs/src/extra_models/tutorial003.py @@ -0,0 +1,35 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class BaseItem(BaseModel): + description: str + type: str + + +class CarItem(BaseItem): + type = "car" + + +class PlaneItem(BaseItem): + type = "plane" + size: int + + +items = { + "item1": {"description": "All my friends drive a low rider", "type": "car"}, + "item2": { + "description": "Music is my aeroplane, it's my aeroplane", + "type": "plane", + "size": 5, + }, +} + + +@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem]) +async def read_item(item_id: str): + return items[item_id] diff --git a/docs/src/extra_models/tutorial004.py b/docs/src/extra_models/tutorial004.py new file mode 100644 index 000000000..a8e0f7af5 --- /dev/null +++ b/docs/src/extra_models/tutorial004.py @@ -0,0 +1,22 @@ +from typing import List + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str + + +items = [ + {"name": "Foo", "description": "There comes my hero"}, + {"name": "Red", "description": "It's my aeroplane"}, +] + + +@app.get("/items/", response_model=List[Item]) +async def read_items(): + return items diff --git a/docs/tutorial/extra-models.md b/docs/tutorial/extra-models.md index 04c20ade3..34e6ff0d8 100644 --- a/docs/tutorial/extra-models.md +++ b/docs/tutorial/extra-models.md @@ -152,6 +152,28 @@ That way, we can declare just the differences between the models (with plaintext {!./src/extra_models/tutorial002.py!} ``` +## `Union` or `anyOf` + +You can declare a response to be the `Union` of two types, that means, that the response would be any of the two. + +It will be defined in OpenAPI with `anyOf`. + +To do that, use the standard Python type hint `typing.Union`: + +```Python hl_lines="1 14 15 18 19 20 33" +{!./src/extra_models/tutorial003.py!} +``` + +## List of models + +The same way, you can declare responses of lists of objects. + +For that, use the standard Python `typing.List`: + +```Python hl_lines="1 20" +{!./src/extra_models/tutorial004.py!} +``` + ## Recap Use multiple Pydantic models and inherit freely for each case. diff --git a/tests/test_tutorial/test_extra_models/__init__.py b/tests/test_tutorial/test_extra_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py new file mode 100644 index 000000000..76e124cb8 --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -0,0 +1,125 @@ +from starlette.testclient import TestClient + +from extra_models.tutorial003 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "Fast API", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response_Read_Item", + "anyOf": [ + {"$ref": "#/components/schemas/PlaneItem"}, + {"$ref": "#/components/schemas/CarItem"}, + ], + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Get", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item_Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": ["description", "size"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "plane"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "CarItem": { + "title": "CarItem", + "required": ["description"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "car"}, + }, + }, + "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"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_get_car(): + response = client.get("/items/item1") + assert response.status_code == 200 + assert response.json() == { + "description": "All my friends drive a low rider", + "type": "car", + } + + +def test_get_plane(): + response = client.get("/items/item2") + assert response.status_code == 200 + assert response.json() == { + "description": "Music is my aeroplane, it's my aeroplane", + "type": "plane", + "size": 5, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004.py b/tests/test_tutorial/test_extra_models/test_tutorial004.py new file mode 100644 index 000000000..ac9192291 --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial004.py @@ -0,0 +1,60 @@ +from starlette.testclient import TestClient + +from extra_models.tutorial004 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "Fast API", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response_Read_Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + "summary": "Read Items Get", + "operationId": "read_items_items__get", + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_schema + + +def test_get_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == [ + {"name": "Foo", "description": "There comes my hero"}, + {"name": "Red", "description": "It's my aeroplane"}, + ]