This commit is contained in:
Motov Yurii 2025-12-16 21:09:33 +00:00 committed by GitHub
commit be0f035497
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 251 additions and 10 deletions

View File

@ -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:

View File

@ -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