mirror of https://github.com/tiangolo/fastapi.git
Merge 20527a7148 into 272204c0c7
This commit is contained in:
commit
be0f035497
|
|
@ -234,11 +234,17 @@ def _get_model_config(model: BaseModel) -> Any:
|
||||||
return model.model_config
|
return model.model_config
|
||||||
|
|
||||||
|
|
||||||
|
def _model_has_computed_fields(model_or_enum: Any) -> bool:
|
||||||
|
if lenient_issubclass(model_or_enum, BaseModel):
|
||||||
|
model_schema = model_or_enum.__pydantic_core_schema__.get("schema", {})
|
||||||
|
computed_fields = model_schema.get("computed_fields", [])
|
||||||
|
return len(computed_fields) > 0
|
||||||
|
return False # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
def _has_computed_fields(field: ModelField) -> bool:
|
def _has_computed_fields(field: ModelField) -> bool:
|
||||||
computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
|
models = get_flat_models_from_field(field, known_models=set())
|
||||||
"computed_fields", []
|
return any(_model_has_computed_fields(model) for model in models)
|
||||||
)
|
|
||||||
return len(computed_fields) > 0
|
|
||||||
|
|
||||||
|
|
||||||
def get_schema_from_model_field(
|
def get_schema_from_model_field(
|
||||||
|
|
@ -250,17 +256,18 @@ def get_schema_from_model_field(
|
||||||
],
|
],
|
||||||
separate_input_output_schemas: bool = True,
|
separate_input_output_schemas: bool = True,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
override_mode: Union[Literal["validation"], None] = (
|
override_mode: Union[Literal["validation"], None] = None
|
||||||
None
|
if not separate_input_output_schemas:
|
||||||
if (separate_input_output_schemas or _has_computed_fields(field))
|
override_mode = (
|
||||||
else "validation"
|
None
|
||||||
)
|
if (separate_input_output_schemas or _has_computed_fields(field))
|
||||||
|
else "validation"
|
||||||
|
)
|
||||||
field_alias = (
|
field_alias = (
|
||||||
(field.validation_alias or field.alias)
|
(field.validation_alias or field.alias)
|
||||||
if field.mode == "validation"
|
if field.mode == "validation"
|
||||||
else (field.serialization_alias or field.alias)
|
else (field.serialization_alias or field.alias)
|
||||||
)
|
)
|
||||||
|
|
||||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||||
json_schema = field_mapping[(field, override_mode or field.mode)]
|
json_schema = field_mapping[(field, override_mode or field.mode)]
|
||||||
if "$ref" not in json_schema:
|
if "$ref" not in json_schema:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .utils import needs_pydanticv2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="client", params=[True, False])
|
||||||
|
def get_client(request: pytest.FixtureRequest):
|
||||||
|
from pydantic import computed_field
|
||||||
|
|
||||||
|
class MyModel(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def is_adult(self) -> bool:
|
||||||
|
return self.age >= 18
|
||||||
|
|
||||||
|
app = FastAPI(separate_input_output_schemas=request.param)
|
||||||
|
|
||||||
|
@app.get("/list")
|
||||||
|
def get_items() -> List[MyModel]:
|
||||||
|
return [MyModel(id=1, name="Alice", age=30), MyModel(id=2, name="Bob", age=17)]
|
||||||
|
|
||||||
|
@app.post("/item")
|
||||||
|
def create_item(item: MyModel) -> MyModel:
|
||||||
|
return item
|
||||||
|
|
||||||
|
yield TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@needs_pydanticv2
|
||||||
|
def test_create_item(client: TestClient):
|
||||||
|
response = client.post(
|
||||||
|
"/item",
|
||||||
|
json={"id": 1, "name": "Alice", "age": 30},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == {"id": 1, "name": "Alice", "age": 30, "is_adult": True}
|
||||||
|
|
||||||
|
|
||||||
|
@needs_pydanticv2
|
||||||
|
def test_get_items(client: TestClient):
|
||||||
|
response = client.get("/list")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == [
|
||||||
|
{"id": 1, "name": "Alice", "age": 30, "is_adult": True},
|
||||||
|
{"id": 2, "name": "Bob", "age": 17, "is_adult": False},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@needs_pydanticv2
|
||||||
|
def test_openapi(client: TestClient):
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
openapi_schema = response.json()
|
||||||
|
expected_schema = {
|
||||||
|
"info": {
|
||||||
|
"title": "FastAPI",
|
||||||
|
"version": "0.1.0",
|
||||||
|
},
|
||||||
|
"openapi": "3.1.0",
|
||||||
|
"paths": {
|
||||||
|
"/item": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "create_item_item_post",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/MyModel-Input",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/MyModel-Output",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "Successful Response",
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "Validation Error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"summary": "Create Item",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/list": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "get_items_list_get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/MyModel-Output",
|
||||||
|
},
|
||||||
|
"title": "Response Get Items List Get",
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "Successful Response",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"summary": "Get Items",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ValidationError",
|
||||||
|
},
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"MyModel-Input": {
|
||||||
|
"properties": {
|
||||||
|
"age": {
|
||||||
|
"title": "Age",
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"title": "Id",
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"title": "Name",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"age",
|
||||||
|
],
|
||||||
|
"title": "MyModel",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"MyModel-Output": {
|
||||||
|
"properties": {
|
||||||
|
"age": {
|
||||||
|
"title": "Age",
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"title": "Id",
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
"is_adult": {
|
||||||
|
"readOnly": True,
|
||||||
|
"title": "Is Adult",
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"title": "Name",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"age",
|
||||||
|
"is_adult",
|
||||||
|
],
|
||||||
|
"title": "MyModel",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
"ValidationError": {
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"title": "Message",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"title": "Error Type",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"loc",
|
||||||
|
"msg",
|
||||||
|
"type",
|
||||||
|
],
|
||||||
|
"title": "ValidationError",
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert openapi_schema == expected_schema
|
||||||
Loading…
Reference in New Issue