]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix `separate_input_output_schemas=False` with `computed_field` (#14453)
authorMotov Yurii <109919500+YuriiMotov@users.noreply.github.com>
Fri, 5 Dec 2025 20:19:30 +0000 (21:19 +0100)
committerGitHub <noreply@github.com>
Fri, 5 Dec 2025 20:19:30 +0000 (21:19 +0100)
fastapi/_compat/v2.py
tests/test_openapi_separate_input_output_schemas.py

index 0faa7d5a8d8d600eb01f0e592295cf36730abd69..acd23d8465f98da39300dae845ba86871e57cd45 100644 (file)
@@ -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)
index fa73620eae379d88850b1ea80ae4194342541612..c9a05418bf1ceec047355dfbd3081b079818fb6f 100644 (file)
@@ -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": {