mirror of https://github.com/tiangolo/fastapi.git
🐛 Fix `separate_input_output_schemas=False` with `computed_field` (#14453)
This commit is contained in:
parent
f0dd1046a6
commit
812a1926f0
|
|
@ -171,6 +171,13 @@ def _get_model_config(model: BaseModel) -> Any:
|
|||
return model.model_config
|
||||
|
||||
|
||||
def _has_computed_fields(field: ModelField) -> bool:
|
||||
computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
|
||||
"computed_fields", []
|
||||
)
|
||||
return len(computed_fields) > 0
|
||||
|
||||
|
||||
def get_schema_from_model_field(
|
||||
*,
|
||||
field: ModelField,
|
||||
|
|
@ -180,12 +187,9 @@ def get_schema_from_model_field(
|
|||
],
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
computed_fields = field._type_adapter.core_schema.get("schema", {}).get(
|
||||
"computed_fields", []
|
||||
)
|
||||
override_mode: Union[Literal["validation"], None] = (
|
||||
None
|
||||
if (separate_input_output_schemas or len(computed_fields) > 0)
|
||||
if (separate_input_output_schemas or _has_computed_fields(field))
|
||||
else "validation"
|
||||
)
|
||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||
|
|
@ -208,15 +212,7 @@ def get_definitions(
|
|||
Dict[Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue],
|
||||
Dict[str, Dict[str, Any]],
|
||||
]:
|
||||
has_computed_fields: bool = any(
|
||||
field._type_adapter.core_schema.get("schema", {}).get("computed_fields", [])
|
||||
for field in fields
|
||||
)
|
||||
|
||||
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
||||
override_mode: Union[Literal["validation"], None] = (
|
||||
None if (separate_input_output_schemas or has_computed_fields) else "validation"
|
||||
)
|
||||
validation_fields = [field for field in fields if field.mode == "validation"]
|
||||
serialization_fields = [field for field in fields if field.mode == "serialization"]
|
||||
flat_validation_models = get_flat_models_from_fields(
|
||||
|
|
@ -246,9 +242,16 @@ def get_definitions(
|
|||
unique_flat_model_fields = {
|
||||
f for f in flat_model_fields if f.type_ not in input_types
|
||||
}
|
||||
|
||||
inputs = [
|
||||
(field, override_mode or field.mode, field._type_adapter.core_schema)
|
||||
(
|
||||
field,
|
||||
(
|
||||
field.mode
|
||||
if (separate_input_output_schemas or _has_computed_fields(field))
|
||||
else "validation"
|
||||
),
|
||||
field._type_adapter.core_schema,
|
||||
)
|
||||
for field in list(fields) + list(unique_flat_model_fields)
|
||||
]
|
||||
field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,18 @@ class Item(BaseModel):
|
|||
model_config = {"json_schema_serialization_defaults_required": True}
|
||||
|
||||
|
||||
if PYDANTIC_V2:
|
||||
from pydantic import computed_field
|
||||
|
||||
class WithComputedField(BaseModel):
|
||||
name: str
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def computed_field(self) -> str:
|
||||
return f"computed {self.name}"
|
||||
|
||||
|
||||
def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
|
||||
app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)
|
||||
|
||||
|
|
@ -46,6 +58,14 @@ def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
|
|||
Item(name="Plumbus"),
|
||||
]
|
||||
|
||||
if PYDANTIC_V2:
|
||||
|
||||
@app.post("/with-computed-field/")
|
||||
def create_with_computed_field(
|
||||
with_computed_field: WithComputedField,
|
||||
) -> WithComputedField:
|
||||
return with_computed_field
|
||||
|
||||
client = TestClient(app)
|
||||
return client
|
||||
|
||||
|
|
@ -131,6 +151,23 @@ def test_read_items():
|
|||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_with_computed_field():
|
||||
client = get_app_client()
|
||||
client_no = get_app_client(separate_input_output_schemas=False)
|
||||
response = client.post("/with-computed-field/", json={"name": "example"})
|
||||
response2 = client_no.post("/with-computed-field/", json={"name": "example"})
|
||||
assert response.status_code == response2.status_code == 200, response.text
|
||||
assert (
|
||||
response.json()
|
||||
== response2.json()
|
||||
== {
|
||||
"name": "example",
|
||||
"computed_field": "computed example",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_openapi_schema():
|
||||
client = get_app_client()
|
||||
|
|
@ -245,6 +282,44 @@ def test_openapi_schema():
|
|||
},
|
||||
}
|
||||
},
|
||||
"/with-computed-field/": {
|
||||
"post": {
|
||||
"summary": "Create With Computed Field",
|
||||
"operationId": "create_with_computed_field_with_computed_field__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WithComputedField-Input"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WithComputedField-Output"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
|
|
@ -333,6 +408,25 @@ def test_openapi_schema():
|
|||
"required": ["subname", "sub_description", "tags"],
|
||||
"title": "SubItem",
|
||||
},
|
||||
"WithComputedField-Input": {
|
||||
"properties": {"name": {"type": "string", "title": "Name"}},
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"title": "WithComputedField",
|
||||
},
|
||||
"WithComputedField-Output": {
|
||||
"properties": {
|
||||
"name": {"type": "string", "title": "Name"},
|
||||
"computed_field": {
|
||||
"type": "string",
|
||||
"title": "Computed Field",
|
||||
"readOnly": True,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["name", "computed_field"],
|
||||
"title": "WithComputedField",
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
|
|
@ -458,6 +552,44 @@ def test_openapi_schema_no_separate():
|
|||
},
|
||||
}
|
||||
},
|
||||
"/with-computed-field/": {
|
||||
"post": {
|
||||
"summary": "Create With Computed Field",
|
||||
"operationId": "create_with_computed_field_with_computed_field__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WithComputedField-Input"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WithComputedField-Output"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
|
|
@ -508,6 +640,25 @@ def test_openapi_schema_no_separate():
|
|||
"required": ["subname"],
|
||||
"title": "SubItem",
|
||||
},
|
||||
"WithComputedField-Input": {
|
||||
"properties": {"name": {"type": "string", "title": "Name"}},
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"title": "WithComputedField",
|
||||
},
|
||||
"WithComputedField-Output": {
|
||||
"properties": {
|
||||
"name": {"type": "string", "title": "Name"},
|
||||
"computed_field": {
|
||||
"type": "string",
|
||||
"title": "Computed Field",
|
||||
"readOnly": True,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["name", "computed_field"],
|
||||
"title": "WithComputedField",
|
||||
},
|
||||
"ValidationError": {
|
||||
"properties": {
|
||||
"loc": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue