mirror of https://github.com/tiangolo/fastapi.git
✅ Update response_model_by_alias (#1642)
* Make openapi models honor response_model_by_alias * Add test for response_model_by_alias working with openapi models * ⏪ Revert changes * ✅ Update and extend tests for response_model_by_alias * ⏪ Revert test name change * 📌 Pin Pytest and Pytest-Cov Co-authored-by: Martin Zaťko <martin.zatko@kiwi.com>
This commit is contained in:
parent
50926faead
commit
e5594e860f
|
|
@ -52,7 +52,6 @@ except ImportError: # pragma: nocover
|
||||||
def _prepare_response_content(
|
def _prepare_response_content(
|
||||||
res: Any,
|
res: Any,
|
||||||
*,
|
*,
|
||||||
by_alias: bool = True,
|
|
||||||
exclude_unset: bool,
|
exclude_unset: bool,
|
||||||
exclude_defaults: bool = False,
|
exclude_defaults: bool = False,
|
||||||
exclude_none: bool = False,
|
exclude_none: bool = False,
|
||||||
|
|
@ -60,14 +59,14 @@ def _prepare_response_content(
|
||||||
if isinstance(res, BaseModel):
|
if isinstance(res, BaseModel):
|
||||||
if PYDANTIC_1:
|
if PYDANTIC_1:
|
||||||
return res.dict(
|
return res.dict(
|
||||||
by_alias=by_alias,
|
by_alias=True,
|
||||||
exclude_unset=exclude_unset,
|
exclude_unset=exclude_unset,
|
||||||
exclude_defaults=exclude_defaults,
|
exclude_defaults=exclude_defaults,
|
||||||
exclude_none=exclude_none,
|
exclude_none=exclude_none,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return res.dict(
|
return res.dict(
|
||||||
by_alias=by_alias, skip_defaults=exclude_unset,
|
by_alias=True, skip_defaults=exclude_unset,
|
||||||
) # pragma: nocover
|
) # pragma: nocover
|
||||||
elif isinstance(res, list):
|
elif isinstance(res, list):
|
||||||
return [
|
return [
|
||||||
|
|
@ -108,7 +107,6 @@ async def serialize_response(
|
||||||
errors = []
|
errors = []
|
||||||
response_content = _prepare_response_content(
|
response_content = _prepare_response_content(
|
||||||
response_content,
|
response_content,
|
||||||
by_alias=by_alias,
|
|
||||||
exclude_unset=exclude_unset,
|
exclude_unset=exclude_unset,
|
||||||
exclude_defaults=exclude_defaults,
|
exclude_defaults=exclude_defaults,
|
||||||
exclude_none=exclude_none,
|
exclude_none=exclude_none,
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ Documentation = "https://fastapi.tiangolo.com/"
|
||||||
|
|
||||||
[tool.flit.metadata.requires-extra]
|
[tool.flit.metadata.requires-extra]
|
||||||
test = [
|
test = [
|
||||||
"pytest >=5.4.3",
|
"pytest==5.4.3",
|
||||||
"pytest-cov",
|
"pytest-cov==2.10.0",
|
||||||
"mypy",
|
"mypy",
|
||||||
"black",
|
"black",
|
||||||
"isort",
|
"isort",
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,82 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
def test_get_openapi():
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
class Model(BaseModel):
|
class Model(BaseModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Model2(BaseModel):
|
|
||||||
|
class Model2(BaseModel):
|
||||||
a: Model
|
a: Model
|
||||||
|
|
||||||
class Model3(BaseModel):
|
|
||||||
|
class Model3(BaseModel):
|
||||||
c: Model
|
c: Model
|
||||||
d: Model2
|
d: Model2
|
||||||
|
|
||||||
@app.get("/", response_model=Model3)
|
|
||||||
def f():
|
|
||||||
pass # pragma: no cover
|
|
||||||
|
|
||||||
openapi = app.openapi()
|
@app.get("/", response_model=Model3)
|
||||||
assert isinstance(openapi, dict)
|
def f():
|
||||||
|
return {"c": {}, "d": {"a": {}}}
|
||||||
|
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/": {
|
||||||
|
"get": {
|
||||||
|
"summary": "F",
|
||||||
|
"operationId": "f__get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/Model3"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"Model": {"title": "Model", "type": "object", "properties": {}},
|
||||||
|
"Model2": {
|
||||||
|
"title": "Model2",
|
||||||
|
"required": ["a"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"a": {"$ref": "#/components/schemas/Model"}},
|
||||||
|
},
|
||||||
|
"Model3": {
|
||||||
|
"title": "Model3",
|
||||||
|
"required": ["c", "d"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"c": {"$ref": "#/components/schemas/Model"},
|
||||||
|
"d": {"$ref": "#/components/schemas/Model2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openapi_schema():
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == openapi_schema
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_api_route():
|
||||||
|
response = client.get("/")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"c": {}, "d": {"a": {}}}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,323 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
class Model(BaseModel):
|
||||||
|
name: str = Field(alias="alias")
|
||||||
|
|
||||||
|
|
||||||
|
class ModelNoAlias(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"description": (
|
||||||
|
"response_model_by_alias=False is basically a quick hack, to support "
|
||||||
|
"proper OpenAPI use another model with the correct field names"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/dict", response_model=Model, response_model_by_alias=False)
|
||||||
|
def read_dict():
|
||||||
|
return {"alias": "Foo"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/model", response_model=Model, response_model_by_alias=False)
|
||||||
|
def read_model():
|
||||||
|
return Model(alias="Foo")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/list", response_model=List[Model], response_model_by_alias=False)
|
||||||
|
def read_list():
|
||||||
|
return [{"alias": "Foo"}, {"alias": "Bar"}]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/by-alias/dict", response_model=Model)
|
||||||
|
def by_alias_dict():
|
||||||
|
return {"alias": "Foo"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/by-alias/model", response_model=Model)
|
||||||
|
def by_alias_model():
|
||||||
|
return Model(alias="Foo")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/by-alias/list", response_model=List[Model])
|
||||||
|
def by_alias_list():
|
||||||
|
return [{"alias": "Foo"}, {"alias": "Bar"}]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/no-alias/dict", response_model=ModelNoAlias)
|
||||||
|
def by_alias_dict():
|
||||||
|
return {"name": "Foo"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/no-alias/model", response_model=ModelNoAlias)
|
||||||
|
def by_alias_model():
|
||||||
|
return ModelNoAlias(name="Foo")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/no-alias/list", response_model=List[ModelNoAlias])
|
||||||
|
def by_alias_list():
|
||||||
|
return [{"name": "Foo"}, {"name": "Bar"}]
|
||||||
|
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/dict": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Read Dict",
|
||||||
|
"operationId": "read_dict_dict_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/Model"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/model": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Read Model",
|
||||||
|
"operationId": "read_model_model_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/Model"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/list": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Read List",
|
||||||
|
"operationId": "read_list_list_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"title": "Response Read List List Get",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/Model"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/by-alias/dict": {
|
||||||
|
"get": {
|
||||||
|
"summary": "By Alias Dict",
|
||||||
|
"operationId": "by_alias_dict_by_alias_dict_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/Model"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/by-alias/model": {
|
||||||
|
"get": {
|
||||||
|
"summary": "By Alias Model",
|
||||||
|
"operationId": "by_alias_model_by_alias_model_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/Model"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/by-alias/list": {
|
||||||
|
"get": {
|
||||||
|
"summary": "By Alias List",
|
||||||
|
"operationId": "by_alias_list_by_alias_list_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"title": "Response By Alias List By Alias List Get",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/Model"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/no-alias/dict": {
|
||||||
|
"get": {
|
||||||
|
"summary": "By Alias Dict",
|
||||||
|
"operationId": "by_alias_dict_no_alias_dict_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/ModelNoAlias"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/no-alias/model": {
|
||||||
|
"get": {
|
||||||
|
"summary": "By Alias Model",
|
||||||
|
"operationId": "by_alias_model_no_alias_model_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/ModelNoAlias"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/no-alias/list": {
|
||||||
|
"get": {
|
||||||
|
"summary": "By Alias List",
|
||||||
|
"operationId": "by_alias_list_no_alias_list_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"title": "Response By Alias List No Alias List Get",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ModelNoAlias"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"Model": {
|
||||||
|
"title": "Model",
|
||||||
|
"required": ["alias"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"alias": {"title": "Alias", "type": "string"}},
|
||||||
|
},
|
||||||
|
"ModelNoAlias": {
|
||||||
|
"title": "ModelNoAlias",
|
||||||
|
"required": ["name"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"name": {"title": "Name", "type": "string"}},
|
||||||
|
"description": "response_model_by_alias=False is basically a quick hack, to support proper OpenAPI use another model with the correct field names",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openapi_schema():
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == openapi_schema
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_dict():
|
||||||
|
response = client.get("/dict")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"name": "Foo"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_model():
|
||||||
|
response = client.get("/model")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"name": "Foo"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_list():
|
||||||
|
response = client.get("/list")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == [
|
||||||
|
{"name": "Foo"},
|
||||||
|
{"name": "Bar"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_dict_by_alias():
|
||||||
|
response = client.get("/by-alias/dict")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"alias": "Foo"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_model_by_alias():
|
||||||
|
response = client.get("/by-alias/model")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"alias": "Foo"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_list_by_alias():
|
||||||
|
response = client.get("/by-alias/list")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == [
|
||||||
|
{"alias": "Foo"},
|
||||||
|
{"alias": "Bar"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_dict_no_alias():
|
||||||
|
response = client.get("/no-alias/dict")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"name": "Foo"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_model_no_alias():
|
||||||
|
response = client.get("/no-alias/model")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"name": "Foo"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_list_no_alias():
|
||||||
|
response = client.get("/no-alias/list")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == [
|
||||||
|
{"name": "Foo"},
|
||||||
|
{"name": "Bar"},
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue