8000 🐛 Fix `separate_input_output_schemas=False` with `computed_field` by YuriiMotov · Pull Request #14453 · fastapi/fastapi · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions fastapi/_compat/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
151 changes: 151 additions & 0 deletions tests/test_openapi_separate_input_output_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -46,6 +58,14 @@ def read_items() -> List[Item]:
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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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",
"req 8000 uestBody": {
"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": {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down
0