← pod_r5_v1i

fastapi_13207

resolved RESOLVED UNSUBMITTED PASS · 32 tool calls · 300 s · fastapi/fastapi

Task input

🐛 Fix OpenAPI schema support for computed fields when using `separate_input_output_schemas=False`

This demonstrates how the output schema differs when the FastAPI app runs with different values for `separate_input_output_schemas` - namely, the computed fields are omitted.

Initially reported here: https://github.com/fastapi/fastapi/discussions/10342

Tool calls (32)

#ToolArgumentsResult
1run_command
{
 "command": "( grep -rn 'separate_input_output_schemas' --include=*.py . || echo '<IMPORTANT> NO MATCH FOUND. DO NOT TRY SAME COMMAND.' ) | head -40"
}
{"status": "ok", "stdout": "./tests/test_openapi_separate_input_output_schemas.py:27:def get_app_client(separate_input_output_schemas: bool = True) -> TestClien
full
{"status": "ok", "stdout": "./tests/test_openapi_separate_input_output_schemas.py:27:def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:\n./tests/test_openapi_separate_input_output_schemas.py:28:    app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)\n./tests/test_openapi_separate_input_output_schemas.py:55:    client_no = get_app_client(separate_input_output_schemas=False)\n./tests/test_openapi_separate_input_output_schemas.py:68:    client_no = get_app_client(separate_input_output_schemas=False)\n./tests/test_openapi_separate_input_output_schemas.py:89:    client_no = get_app_client(separate_input_output_schemas=False)\n./tests/test_openapi_separate_input_output_schemas.py:116:    client_no = get_app_client(separate_input_output_schemas=False)\n./tests/test_openapi_separate_input_output_schemas.py:360:    client = get_app_client(separate_input_output_schemas=False)\n./fastapi/applications.py:791:        separate_input_output_schemas: Annotated[\n./fastapi/applications.py:875:        self.separate_input_output_schemas = separate_input_output_schemas\n./fastapi/applications.py:1078:                separate_input_output_schemas=self.separate_input_output_schemas,\n./fastapi/openapi/utils.py:102:    separate_input_output_schemas: bool = True,\n./fastapi/openapi/utils.py:133:                separate_input_output_schemas=separate_input_output_schemas,\n./fastapi/openapi/utils.py:175:    separate_input_output_schemas: bool = True,\n./fastapi/openapi/utils.py:184:        separate_input_output_schemas=separate_input_output_schemas,\n./fastapi/openapi/utils.py:258:    separate_input_output_schemas: bool = True,\n./fastapi/openapi/utils.py:288:                separate_input_output_schemas=separate_input_output_schemas,\n./fastapi/openapi/utils.py:309:                    separate_input_output_schemas=separate_input_output_schemas,\n./fastapi/openapi/utils.py:326:                            separate_input_output_schemas=separate
2run_command
{
 "command": "cat > /tmp/repro.py <<'PY'\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, computed_field\nfrom fastapi.testclient import TestClient\nimport json\n\nclass Item(BaseModel):\n    name: str\n    price: float\n\n    @computed_field\n    @property\n    def total(self) -> float:\n        return self.price * 1.1\n\napp = FastAPI(separate_input_output_schemas=False)\n\n@app.get(\"/item\", response_model=Item)\ndef get_item():\n    return Item(name=\"Test\", price=100.0)\n\nclient = TestClient(app)\n\ndef test_openapi_computed_field():\n    response = client.get(\"/openapi.json\")\n    schema = response.json()\n    \n    # Find the Item schema in components/schemas\n    schemas = schema.get(\"components\", {}).get(\"schemas\", {})\n    item_schema = schemas.get(\"Item\")\n    \n    if not item_schema:\n        print(\"Item schema not found!\")\n        return\n        \n    properties = item_schema.get(\"properties\", {})\n    print(f\"Properties: {list(properties.keys())}\")\n    \n    if \"total\" in properties:\n        print(\"SUCCESS: 'total' computed field is present in OpenAPI schema\")\n    else:\n        print(\"FAILURE: 'total' computed field is MISSING from OpenAPI schema\")\n\nif __name__ == \"__main__\":\n    test_openapi_computed_field()\nPY\npython3 /tmp/repro.py\n"
}
{"status": "ok", "stdout": "Properties: ['name', 'price']\nFAILURE: 'total' computed field is MISSING from OpenAPI schema\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Properties: ['name', 'price']\nFAILURE: 'total' computed field is MISSING from OpenAPI schema\n", "stderr": "", "exit_code": 0}
3run_command
{
 "command": "# 1: let's try with separate_input_output_schemas=True to see if it works\ncat > /tmp/repro.py <<'PY'\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, computed_field\nfrom fastapi.testclient import TestClient\nimport json\n\nclass Item(BaseModel):\n    name: str\n    price: float\n\n    @computed_field\n    @property\n    def total(self) -> float:\n        return self.price * 1.1\n\napp = FastAPI(separate_input_output_schemas=True)\n\n@app.get(\"/item\", response_model=Item)\ndef get_item():\n    return Item(name=\"Test\", price=100.0)\n\nclient = TestClient(app)\n\ndef test_openapi_computed_field():\n    response = client.get(\"/openapi.json\")\n    schema = response.json()\n    \n    # Find the Item schema in components/schemas\n    schemas = schema.get(\"components\", {}).get(\"schemas\", {})\n    item_schema = schemas.get(\"Item\")\n    \n    if not item_schema:\n        print(\"Item schema not found!\")\n        return\n        \n    properties = item_schema.get(\"properties\", {})\n    print(f\"Properties: {list(properties.keys())}\")\n    \n    if \"total\" in properties:\n        print(\"SUCCESS: 'total' computed field is present in OpenAPI schema\")\n    else:\n        print(\"FAILURE: 'total' computed field is MISSING from OpenAPI schema\")\n\nif __name__ == \"__main__\":\n    test_openapi_computed_field()\nPY\npython3 /tmp/repro.py\n"
}
{"status": "ok", "stdout": "Properties: ['name', 'price', 'total']\nSUCCESS: 'total' computed field is present in OpenAPI schema\n", "stderr": "", "exit_code":
full
{"status": "ok", "stdout": "Properties: ['name', 'price', 'total']\nSUCCESS: 'total' computed field is present in OpenAPI schema\n", "stderr": "", "exit_code": 0}
4run_command
{
 "command": "# 2: Inspect fastapi/openapi/utils.py to see how schemas are generated\nsed -n '250,350p' fastapi/openapi/utils.py"
}
{"status": "ok", "stdout": "def get_openapi_path(\n *,\n route: routing.APIRoute,\n operation_ids: Set[str],\n model_name_map: ModelNameMap,\n field_mapping: Di
full
{"status": "ok", "stdout": "def get_openapi_path(\n    *,\n    route: routing.APIRoute,\n    operation_ids: Set[str],\n    model_name_map: ModelNameMap,\n    field_mapping: Dict[\n        Tuple[ModelField, Literal[\"validation\", \"serialization\"]], JsonSchemaValue\n    ],\n    separate_input_output_schemas: bool = True,\n) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:\n    path = {}\n    security_schemes: Dict[str, Any] = {}\n    definitions: Dict[str, Any] = {}\n    assert route.methods is not None, \"Methods must be a list\"\n    if isinstance(route.response_class, DefaultPlaceholder):\n        current_response_class: Type[Response] = route.response_class.value\n    else:\n        current_response_class = route.response_class\n    assert current_response_class, \"A response class is needed to generate OpenAPI\"\n    route_response_media_type: Optional[str] = current_response_class.media_type\n    if route.include_in_schema:\n        for method in route.methods:\n            operation = get_openapi_operation_metadata(\n                route=route, method=method, operation_ids=operation_ids\n            )\n            parameters: List[Dict[str, Any]] = []\n            flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)\n            security_definitions, operation_security = get_openapi_security_definitions(\n                flat_dependant=flat_dependant\n            )\n            if operation_security:\n                operation.setdefault(\"security\", []).extend(operation_security)\n            if security_definitions:\n                security_schemes.update(security_definitions)\n            operation_parameters = _get_openapi_operation_parameters(\n                dependant=route.dependant,\n                model_name_map=model_name_map,\n                field_mapping=field_mapping,\n                separate_input_output_schemas=separate_input_output_schemas,\n            )\n            parameters.extend(operation_parameters)\
5run_command
{
 "command": "grep -n \"get_openapi_operation_responses\" fastapi/openapi/utils.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
full
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
6run_command
{
 "command": "grep -n \"get_openapi_responses\" fastapi/openapi/utils.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
full
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
7run_command
{
 "command": "# 3: Let's search for where response models are handled in fastapi/openapi/utils.py\ngrep -n \"response_model\" fastapi/openapi/utils.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
full
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
8run_command
{
 "command": "# 4: Let's search for where the response bodies are handled\ngrep -n \"responses\" fastapi/openapi/utils.py"
}
{"status": "ok", "stdout": "27:from fastapi.responses import Response\n35:from starlette.responses import JSONResponse\n337: # responses in Starlette\n343: oper
full
{"status": "ok", "stdout": "27:from fastapi.responses import Response\n35:from starlette.responses import JSONResponse\n337:                # responses in Starlette\n343:            operation.setdefault(\"responses\", {}).setdefault(status_code, {})[\n360:                operation.setdefault(\"responses\", {}).setdefault(\n365:            if route.responses:\n366:                operation_responses = operation.setdefault(\"responses\", {})\n370:                ) in route.responses.items():\n376:                    openapi_response = operation_responses.setdefault(\n400:                    ) or http.client.responses.get(int(additional_status_code))\n412:                status in operation[\"responses\"]\n415:                operation[\"responses\"][http422] = {\n440:    responses_from_routes: List[ModelField] = []\n453:                responses_from_routes.append(route.response_field)\n455:                responses_from_routes.extend(route.response_fields.values())\n462:        body_fields_from_routes + responses_from_routes + request_fields_from_routes\n", "stderr": "", "exit_code": 0}
9run_command
{
 "command": "# 5: Let's look around line 360 of fastapi/openapi/utils.py\nsed -n '360,410p' fastapi/openapi/utils.py"
}
{"status": "ok", "stdout": " operation.setdefault(\"responses\", {}).setdefault(\n status_code, {}\n ).setdefault(\"content\", {}).setdefault(route_response_med
full
{"status": "ok", "stdout": "                operation.setdefault(\"responses\", {}).setdefault(\n                    status_code, {}\n                ).setdefault(\"content\", {}).setdefault(route_response_media_type, {})[\n                    \"schema\"\n                ] = response_schema\n            if route.responses:\n                operation_responses = operation.setdefault(\"responses\", {})\n                for (\n                    additional_status_code,\n                    additional_response,\n                ) in route.responses.items():\n                    process_response = additional_response.copy()\n                    process_response.pop(\"model\", None)\n                    status_code_key = str(additional_status_code).upper()\n                    if status_code_key == \"DEFAULT\":\n                        status_code_key = \"default\"\n                    openapi_response = operation_responses.setdefault(\n                        status_code_key, {}\n                    )\n                    assert isinstance(process_response, dict), (\n                        \"An additional response must be a dict\"\n                    )\n                    field = route.response_fields.get(additional_status_code)\n                    additional_field_schema: Optional[Dict[str, Any]] = None\n                    if field:\n                        additional_field_schema = get_schema_from_model_field(\n                            field=field,\n                            model_name_map=model_name_map,\n                            field_mapping=field_mapping,\n                            separate_input_output_schemas=separate_input_output_schemas,\n                        )\n                        media_type = route_response_media_type or \"application/json\"\n                        additional_schema = (\n                            process_response.setdefault(\"content\", {})\n                            .setdefault(media_type, {})\n                   
10run_command
{
 "command": "# 6: I see `response_schema` being assigned. Let's find where it's defined.\nsed -n '340,360p' fastapi/openapi/utils.py"
}
{"status": "ok", "stdout": " if status_code_param is not None:\n if isinstance(status_code_param.default, int):\n status_code = str(status_code_param.default)\n
full
{"status": "ok", "stdout": "                if status_code_param is not None:\n                    if isinstance(status_code_param.default, int):\n                        status_code = str(status_code_param.default)\n            operation.setdefault(\"responses\", {}).setdefault(status_code, {})[\n                \"description\"\n            ] = route.response_description\n            if route_response_media_type and is_body_allowed_for_status_code(\n                route.status_code\n            ):\n                response_schema = {\"type\": \"string\"}\n                if lenient_issubclass(current_response_class, JSONResponse):\n                    if route.response_field:\n                        response_schema = get_schema_from_model_field(\n                            field=route.response_field,\n                            model_name_map=model_name_map,\n                            field_mapping=field_mapping,\n                            separate_input_output_schemas=separate_input_output_schemas,\n                        )\n                    else:\n                        response_schema = {}\n                operation.setdefault(\"responses\", {}).setdefault(\n", "stderr": "", "exit_code": 0}
11run_command
{
 "command": "# 7: Now let's look at get_schema_from_model_field in fastapi/openapi/utils.py\ngrep -n \"def get_schema_from_model_field\" fastapi/openapi/utils.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
full
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
12run_command
{
 "command": "# 8: Search for get_schema_from_model_field in the whole directory just in case\ngrep -rn \"def get_schema_from_model_field\" ."
}
{"status": "ok", "stdout": "./fastapi/_compat/v1.py:236:def get_schema_from_model_field(\n./fastapi/_compat/main.py:314:def get_schema_from_model_field(\n./fast
full
{"status": "ok", "stdout": "./fastapi/_compat/v1.py:236:def get_schema_from_model_field(\n./fastapi/_compat/main.py:314:def get_schema_from_model_field(\n./fastapi/_compat/v2.py:174:def get_schema_from_model_field(\n", "stderr": "", "exit_code": 0}
13run_command
{
 "command": "# 9: It seems get_schema_from_model_field is in _compat.\n# Let's check fastapi/_compat/v2.py as it's more likely for Pydantic v2\nsed -n '174,230p' fastapi/_compat/v2.py"
}
{"status": "ok", "stdout": "def get_schema_from_model_field(\n *,\n field: ModelField,\n model_name_map: ModelNameMap,\n field_mapping: Dict[\n Tuple[ModelField
full
{"status": "ok", "stdout": "def get_schema_from_model_field(\n    *,\n    field: ModelField,\n    model_name_map: ModelNameMap,\n    field_mapping: Dict[\n        Tuple[ModelField, Literal[\"validation\", \"serialization\"]], JsonSchemaValue\n    ],\n    separate_input_output_schemas: bool = True,\n) -> Dict[str, Any]:\n    override_mode: Union[Literal[\"validation\"], None] = (\n        None if separate_input_output_schemas else \"validation\"\n    )\n    # This expects that GenerateJsonSchema was already used to generate the definitions\n    json_schema = field_mapping[(field, override_mode or field.mode)]\n    if \"$ref\" not in json_schema:\n        # TODO remove when deprecating Pydantic v1\n        # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207\n        json_schema[\"title\"] = field.field_info.title or field.alias.title().replace(\n            \"_\", \" \"\n        )\n    return json_schema\n\n\ndef get_definitions(\n    *,\n    fields: Sequence[ModelField],\n    model_name_map: ModelNameMap,\n    separate_input_output_schemas: bool = True,\n) -> Tuple[\n    Dict[Tuple[ModelField, Literal[\"validation\", \"serialization\"]], JsonSchemaValue],\n    Dict[str, Dict[str, Any]],\n]:\n    schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)\n    override_mode: Union[Literal[\"validation\"], None] = (\n        None if separate_input_output_schemas else \"validation\"\n    )\n    validation_fields = [field for field in fields if field.mode == \"validation\"]\n    serialization_fields = [field for field in fields if field.mode == \"serialization\"]\n    flat_validation_models = get_flat_models_from_fields(\n        validation_fields, known_models=set()\n    )\n    flat_serialization_models = get_flat_models_from_fields(\n        serialization_fields, known_models=set()\n    )\n    flat_validation_model_fields = [\n        ModelField(\n            field_info=FieldInfo(annotation=model),\n   
14run_command
{
 "command": "# 10: Let's see the GenerateJsonSchema class in fastapi/_compat/v2.py\ngrep -n \"class GenerateJsonSchema\" fastapi/_compat/v2.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
full
{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}
15run_command
{
 "command": "# 11: search for GenerateJsonSchema in the whole directory\ngrep -rn \"class GenerateJsonSchema\" ."
}
{"status": "ok", "stdout": "./fastapi/_compat/v1.py:149:class GenerateJsonSchema:\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "./fastapi/_compat/v1.py:149:class GenerateJsonSchema:\n", "stderr": "", "exit_code": 0}
16run_command
{
 "command": "# 12: let's check fastapi/_compat/v1.py\nsed -n '149,300p' fastapi/_compat/v1.py"
}
{"status": "ok", "stdout": "class GenerateJsonSchema:\n ref_template: str\n\n\nclass PydanticSchemaGenerationError(Exception):\n pass\n\n\nRequestErrorModel: Ty
full
{"status": "ok", "stdout": "class GenerateJsonSchema:\n    ref_template: str\n\n\nclass PydanticSchemaGenerationError(Exception):\n    pass\n\n\nRequestErrorModel: Type[BaseModel] = create_model(\"Request\")\n\n\ndef with_info_plain_validator_function(\n    function: Callable[..., Any],\n    *,\n    ref: Union[str, None] = None,\n    metadata: Any = None,\n    serialization: Any = None,\n) -> Any:\n    return {}\n\n\ndef get_model_definitions(\n    *,\n    flat_models: Set[Union[Type[BaseModel], Type[Enum]]],\n    model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],\n) -> Dict[str, Any]:\n    definitions: Dict[str, Dict[str, Any]] = {}\n    for model in flat_models:\n        m_schema, m_definitions, m_nested_models = model_process_schema(\n            model, model_name_map=model_name_map, ref_prefix=REF_PREFIX\n        )\n        definitions.update(m_definitions)\n        model_name = model_name_map[model]\n        definitions[model_name] = m_schema\n    for m_schema in definitions.values():\n        if \"description\" in m_schema:\n            m_schema[\"description\"] = m_schema[\"description\"].split(\"\\f\")[0]\n    return definitions\n\n\ndef is_pv1_scalar_field(field: ModelField) -> bool:\n    from fastapi import params\n\n    field_info = field.field_info\n    if not (\n        field.shape == SHAPE_SINGLETON\n        and not lenient_issubclass(field.type_, BaseModel)\n        and not lenient_issubclass(field.type_, dict)\n        and not shared.field_annotation_is_sequence(field.type_)\n        and not is_dataclass(field.type_)\n        and not isinstance(field_info, params.Body)\n    ):\n        return False\n    if field.sub_fields:\n        if not all(is_pv1_scalar_field(f) for f in field.sub_fields):\n            return False\n    return True\n\n\ndef is_pv1_scalar_sequence_field(field: ModelField) -> bool:\n    if (field.shape in sequence_shapes) and not lenient_issubclass(\n        field.type_, BaseModel\n    ):\n        if field.sub_fields is
17run_command
{
 "command": "# 13: I'm confused. GenerateJsonSchema was declared but not defined in v1.py (it was empty in sed output).\n# Let's search for GenerateJsonSchema definition again.\ngrep -rn \"class GenerateJsonSchema\" ."
}
{"status": "ok", "stdout": "./fastapi/_compat/v1.py:149:class GenerateJsonSchema:\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "./fastapi/_compat/v1.py:149:class GenerateJsonSchema:\n", "stderr": "", "exit_code": 0}
18run_command
{
 "command": "# 14: Let's check around line 149 of fastapi/_compat/v1.py again.\nsed -n '140,160p' fastapi/_compat/v1.py"
}
{"status": "ok", "stdout": " SHAPE_LIST: list,\n SHAPE_SET: set,\n SHAPE_TUPLE: tuple,\n SHAPE_SEQUENCE: list,\n SHAPE_TUPLE_ELLIPSIS: list,\n}\n\n\n@dataclass\
full
{"status": "ok", "stdout": "    SHAPE_LIST: list,\n    SHAPE_SET: set,\n    SHAPE_TUPLE: tuple,\n    SHAPE_SEQUENCE: list,\n    SHAPE_TUPLE_ELLIPSIS: list,\n}\n\n\n@dataclass\nclass GenerateJsonSchema:\n    ref_template: str\n\n\nclass PydanticSchemaGenerationError(Exception):\n    pass\n\n\nRequestErrorModel: Type[BaseModel] = create_model(\"Request\")\n\n\ndef with_info_plain_validator_function(\n", "stderr": "", "exit_code": 0}
19run_command
{
 "command": "# 15: Wait, I see it's just a dataclass. Where is it used?\ngrep -rn \"GenerateJsonSchema\" ."
}
{"status": "ok", "stdout": "./fastapi/_compat/v1.py:149:class GenerateJsonSchema:\n./fastapi/_compat/v2.py:31:from pydantic.json_schema import GenerateJsonSchem
full
{"status": "ok", "stdout": "./fastapi/_compat/v1.py:149:class GenerateJsonSchema:\n./fastapi/_compat/v2.py:31:from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema\n./fastapi/_compat/v2.py:186:    # This expects that GenerateJsonSchema was already used to generate the definitions\n./fastapi/_compat/v2.py:206:    schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)\n", "stderr": "", "exit_code": 0}
20run_command
{
 "command": "# 16: Okay, it's imported from pydantic.json_schema in v2.py.\n# Let's go back to fastapi/_compat/v2.py and look at get_definitions.\nsed -n '195,230p' fastapi/_compat/v2.py"
}
{"status": "ok", "stdout": "\n\ndef get_definitions(\n *,\n fields: Sequence[ModelField],\n model_name_map: ModelNameMap,\n separate_input_output_schemas: bool
full
{"status": "ok", "stdout": "\n\ndef get_definitions(\n    *,\n    fields: Sequence[ModelField],\n    model_name_map: ModelNameMap,\n    separate_input_output_schemas: bool = True,\n) -> Tuple[\n    Dict[Tuple[ModelField, Literal[\"validation\", \"serialization\"]], JsonSchemaValue],\n    Dict[str, Dict[str, Any]],\n]:\n    schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)\n    override_mode: Union[Literal[\"validation\"], None] = (\n        None if separate_input_output_schemas else \"validation\"\n    )\n    validation_fields = [field for field in fields if field.mode == \"validation\"]\n    serialization_fields = [field for field in fields if field.mode == \"serialization\"]\n    flat_validation_models = get_flat_models_from_fields(\n        validation_fields, known_models=set()\n    )\n    flat_serialization_models = get_flat_models_from_fields(\n        serialization_fields, known_models=set()\n    )\n    flat_validation_model_fields = [\n        ModelField(\n            field_info=FieldInfo(annotation=model),\n            name=model.__name__,\n            mode=\"validation\",\n        )\n        for model in flat_validation_models\n    ]\n    flat_serialization_model_fields = [\n        ModelField(\n            field_info=FieldInfo(annotation=model),\n            name=model.__name__,\n            mode=\"serialization\",\n", "stderr": "", "exit_code": 0}
21run_command
{
 "command": "# 17: let's see the rest of get_definitions in fastapi/_compat/v2.py\nsed -n '231,300p' fastapi/_compat/v2.py"
}
{"status": "ok", "stdout": " )\n for model in flat_serialization_models\n ]\n flat_model_fields = flat_validation_model_fields + flat_serialization_model_fields
full
{"status": "ok", "stdout": "        )\n        for model in flat_serialization_models\n    ]\n    flat_model_fields = flat_validation_model_fields + flat_serialization_model_fields\n    input_types = {f.type_ for f in fields}\n    unique_flat_model_fields = {\n        f for f in flat_model_fields if f.type_ not in input_types\n    }\n\n    inputs = [\n        (field, override_mode or field.mode, field._type_adapter.core_schema)\n        for field in list(fields) + list(unique_flat_model_fields)\n    ]\n    field_mapping, definitions = schema_generator.generate_definitions(inputs=inputs)\n    for item_def in cast(Dict[str, Dict[str, Any]], definitions).values():\n        if \"description\" in item_def:\n            item_description = cast(str, item_def[\"description\"]).split(\"\\f\")[0]\n            item_def[\"description\"] = item_description\n    new_mapping, new_definitions = _remap_definitions_and_field_mappings(\n        model_name_map=model_name_map,\n        definitions=definitions,  # type: ignore[arg-type]\n        field_mapping=field_mapping,\n    )\n    return new_mapping, new_definitions\n\n\ndef _replace_refs(\n    *,\n    schema: Dict[str, Any],\n    old_name_to_new_name_map: Dict[str, str],\n) -> Dict[str, Any]:\n    new_schema = deepcopy(schema)\n    for key, value in new_schema.items():\n        if key == \"$ref\":\n            value = schema[\"$ref\"]\n            if isinstance(value, str):\n                ref_name = schema[\"$ref\"].split(\"/\")[-1]\n                if ref_name in old_name_to_new_name_map:\n                    new_name = old_name_to_new_name_map[ref_name]\n                    new_schema[\"$ref\"] = REF_TEMPLATE.format(model=new_name)\n            continue\n        if isinstance(value, dict):\n            new_schema[key] = _replace_refs(\n                schema=value,\n                old_name_to_new_name_map=old_name_to_new_name_map,\n            )\n        elif isinstance(value, list):\n            new_value = []\n            fo
22run_command
{
 "command": "# 18: In get_definitions, if separate_input_output_schemas=False, override_mode is \"validation\".\n# And inputs are passed to schema_generator.generate_definitions:\n# inputs = [\n#     (field, override_mode or field.mode, field._type_adapter.core_schema)\n#     for field in list(fields) + list(unique_flat_model_fields)\n# ]\n# If override_mode is \"validation\", then all fields are processed as \"validation\".\n# In Pydantic v2, computed fields are only present in \"serialization\" mode, not \"validation\" mode.\n# So if separate_input_output_schemas=False, computed fields are omitted because we use \"validation\" mode.\n\n# Let's verify this hypothesis.\n# In Pydantic v2, computed fields are for output.\n# When separate_input_output_schemas=True, we probably use both validation and serialization modes.\n# Let's look at get_definitions again.\n# If separate_input_output_schemas=True, override_mode is None.\n# Then the second element of the tuple in inputs is `field.mode`.\n# ModelField has a mode (\"validation\" or \"serialization\").\n# So it uses both.\n# But if separate_input_output_schemas=False, it uses \"validation\" for everything.\n\n# To fix this, we should probably use \"serialization\" mode if we want the output schema.\n# But the input schema should still use \"validation\" mode.\n# The issue is that when separate_input_output_schemas=False, we are using a single schema for both.\n# FastAPI seems to choose \"validation\" mode as the default for the shared schema.\n# However, the shared schema should ideally be a merge of both, or at least include computed fields if it's used for output.\n\n# Actually, if separate_input_output_schemas=False, the \"validation\" mode schema is used for both.\n# But computed fields are NOT part of the validation schema.\n\n# Let's check if we can change \"validation\" to \"serialization\" when separate_input_output_schemas=False.\n# No, because then we might lose some validation-specific info.\n\n# Wait, if separate_input_output_schemas=False, we want a schema that can be used for both.\n# In Pydantic v2, the \"serialization\" schema usually includes everything that the \"validation\" schema does, plus computed fields.\n\n# Let's try changing \"validation\" to \"serialization\" in fastapi/_compat/v2.py.\n# But wait, the code says:\n# override_mode: Union[Literal[\"validation\"], None] = (\n#     None if separate_input_output_schemas else \"validation\"\n# )\n# Let's try changing \"validation\" to \"serialization\".\n# But \"serialization\" is not a Literal[\"validation\"] in the type hint.\n# Let's check the type hint for override_mode.\n# override_mode: Union[Literal[\"validation\"], None]\n\n# Let's see if we can use \"serialization\" as override_mode.\n# The type hint might just be too restrictive.\n# The inputs are passed to schema_generator.generate_definitions.\n# Let's see if pydantic's GenerateJsonSchema supports \"serialization\".\n# It likely does since it's used for serialization_fields.\n\n# Let's try to change \"validation\" to \"serialization\" and see if it fixes the repro.\n# But before that, let's check if \"serialization\" mode includes all validation fields.\n# In Pydantic v2, it generally does.\n\n# Let's try the change.\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
23run_command
{
 "command": "# 19: I will change \"validation\" to \"serialization\" in fastapi/_compat/v2.py in two places.\n# 1. in get_schema_from_model_field\n# 2. in get_definitions\n\n# Let's check the lines again.\n# get_schema_from_model_field: line 180\n# get_definitions: line 204\n\n# Actually, the type hint for override_mode in get_schema_from_model_field is:\n# override_mode: Union[Literal[\"validation\"], None]\n# And in get_definitions:\n# override_mode: Union[Literal[\"validation\"], None]\n\n# I should also update the type hints if I change the value.\n\n# Wait, let's see if there's a better way.\n# If separate_input_output_schemas=False, we are essentially saying \"I want one schema for both\".\n# If we use \"serialization\" mode, it should include computed fields and all regular fields.\n# If we use \"validation\" mode, it only includes regular fields.\n# So \"serialization\" is what we want for the combined schema.\n\n# Let's apply the change.\npython3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/_compat/v2.py\")\ns = p.read_text()\n\n# Fix get_schema_from_model_field\nold1 = \"\"\"    override_mode: Union[Literal[\"validation\"], None] = (\n        None if separate_input_output_schemas else \"validation\"\n    )\"\"\"\nnew1 = \"\"\"    override_mode: Union[Literal[\"validation\", \"serialization\"], None] = (\n        None if separate_input_output_schemas else \"serialization\"\n    )\"\"\"\ns = s.replace(old1, new1)\n\n# Fix get_definitions\nold2 = \"\"\"    override_mode: Union[Literal[\"validation\"], None] = (\n        None if separate_input_output_schemas else \"validation\"\n    )\"\"\"\n# This is the same as old1, but in get_definitions.\n# Since it's exactly the same string, I should use replace with a count or be more specific.\n# Let's use a more specific replace.\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
24run_command
{
 "command": "# 20: let's use sed or a more precise python script to replace the two occurrences.\n# I'll search for the lines and replace them based on their context.\n\npython3 - <<'PY'\nfrom pathlib import Path\np = Path(\"fastapi/_compat/v2.py\")\ns = p.read_text()\nlines = s.splitlines()\n\n# Find indices of \"override_mode: Union[Literal[\"validation\"], None] = (\"\nindices = [i for i, line in enumerate(lines) if 'override_mode: Union[Literal[\"validation\"], None] = (' in line]\n\n# We expect two indices.\nif len(indices) != 2:\n    print(f\"Expected 2 occurrences, found {len(indices)}\")\n    exit(1)\n\n# Replace first one (get_schema_from_model_field)\nlines[indices[0]] = '    override_mode: Union[Literal[\"validation\", \"serialization\"], None] = ('\nlines[indices[0]+1] = '        None if separate_input_output_schemas else \"serialization\"'\n\n# Replace second one (get_definitions)\nlines[indices[1]] = '    override_mode: Union[Literal[\"validation\", \"serialization\"], None] = ('\nlines[indices[1]+1] = '        None if separate_input_output_schemas else \"serialization\"'\n\np.write_text(\"\\n\".join(lines))\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
25run_command
{
 "command": "# 21: Now let's run the repro script.\npython3 /tmp/repro.py"
}
{"status": "ok", "stdout": "Properties: ['name', 'price', 'total']\nSUCCESS: 'total' computed field is present in OpenAPI schema\n", "stderr": "", "exit_code":
full
{"status": "ok", "stdout": "Properties: ['name', 'price', 'total']\nSUCCESS: 'total' computed field is present in OpenAPI schema\n", "stderr": "", "exit_code": 0}
26run_command
{
 "command": "# 22: The repro now passes. Let's check if it broke anything else.\n# The original issue was specifically about computed fields when separate_input_output_schemas=False.\n# Now let's check if separate_input_output_schemas=True still works as expected.\n# My repro script already checked that (it's a separate test case).\n# Let's run the repro script again to be sure.\npython3 /tmp/repro.py"
}
{"status": "ok", "stdout": "Properties: ['name', 'price', 'total']\nSUCCESS: 'total' computed field is present in OpenAPI schema\n", "stderr": "", "exit_code":
full
{"status": "ok", "stdout": "Properties: ['name', 'price', 'total']\nSUCCESS: 'total' computed field is present in OpenAPI schema\n", "stderr": "", "exit_code": 0}
27run_command
{
 "command": "# 23: wait, I should check if I broke the \"separate_input_output_schemas=True\" case.\n# My repro script only tests the False case now (the one I run at the end).\n# Let me run a script that tests both.\ncat > /tmp/repro_both.py <<'PY'\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel, computed_field\nfrom fastapi.testclient import TestClient\nimport json\n\nclass Item(BaseModel):\n    name: str\n    price: float\n\n    @computed_field\n    @property\n    def total(self) -> float:\n        return self.price * 1.1\n\ndef test_openapi(separate: bool):\n    app = FastAPI(separate_input_output_schemas=separate)\n\n    @app.get(\"/item\", response_model=Item)\n    def get_item():\n        return Item(name=\"Test\", price=100.0)\n\n    client = TestClient(app)\n    response = client.get(\"/openapi.json\")\n    schema = response.json()\n    properties = schema.get(\"components\", {}).get(\"schemas\", {}).get(\"Item\", {}).get(\"properties\", {})\n    print(f\"separate={separate}, properties={list(properties.keys())}\")\n    return list(properties.keys())\n\nif __name__ == \"__main__\":\n    res_false = test_openapi(False)\n    res_true = test_openapi(True)\n    \n    assert \"total\" in res_false, f\"Computed field missing when separate=False: {res_false}\"\n    assert \"total\" in res_true, f\"Computed field missing when separate=True: {res_true}\"\n    print(\"ALL TESTS PASSED\")\nPY\npython3 /tmp/repro_both.py\n"
}
{"status": "ok", "stdout": "separate=False, properties=['name', 'price', 'total']\nseparate=True, properties=['name', 'price', 'total']\nALL TESTS PASSED\n", "s
full
{"status": "ok", "stdout": "separate=False, properties=['name', 'price', 'total']\nseparate=True, properties=['name', 'price', 'total']\nALL TESTS PASSED\n", "stderr": "", "exit_code": 0}
28run_command
{
 "command": "# 24: Let's check if we have any existing tests for separate_input_output_schemas.\n# I saw tests/test_openapi_separate_input_output_schemas.py earlier.\n# Let's run it.\npython -m pytest tests/test_openapi_separate_input_output_schemas.py\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "============================= test session starts ==============================\nplatform d
full
{"status": "error", "error_type": "CommandError", "error_message": "============================= test session starts ==============================\nplatform darwin -- Python 3.13.15, pytest-9.1.1, pluggy-1.6.0\ncodspeed: 5.0.3 (disabled, mode: walltime, callgraph: not supported, timer_resolution: 41.7ns)\nrootdir: /private/workspace\nconfigfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)\nplugins: cov-7.1.0, xdist-3.8.0, timeout-2.4.0, codspeed-5.0.3, sugar-1.1.1, anyio-4.15.1, inline-snapshot-0.35.4\ncollected 6 items\n\ntests/test_openapi_separate_input_output_schemas.py .....F               [100%]\n\n=================================== FAILURES ===================================\n_______________________ test_openapi_schema_no_separate ________________________\n\n    @needs_pydanticv2\n    def test_openapi_schema_no_separate():\n        client = get_app_client(separate_input_output_schemas=False)\n        response = client.get(\"/openapi.json\")\n        assert response.status_code == 200, response.text\n>       assert response.json() == {\n            \"openapi\": \"3.1.0\",\n            \"info\": {\"title\": \"FastAPI\", \"version\": \"0.1.0\"},\n            \"paths\": {\n                \"/items/\": {\n                    \"get\": {\n                        \"summary\": \"Read Items\",\n                        \"operationId\": \"read_items_items__get\",\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Successful Response\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                                            \"items\": {\"$ref\": \"#/components/schemas/Item\"},\n                                            \"type\": \"array\",\n                                            \"title\": \"Response Read Items Items  Get\",\n                
29run_command
{
 "command": "# 25: The test failed because the expected JSON changed.\n# This is expected since I changed the default schema for separate_input_output_schemas=False.\n# I should check if the change is correct for the tests.\n# The failing test is `test_openapi_schema_no_separate`.\n# It expects the schema to be exactly what it was.\n# But the issue is that computed fields WERE missing and they SHOULD be present.\n# So the test itself might need to be updated to expect computed fields if the model has any.\n# Let's see the model used in `tests/test_openapi_separate_input_output_schemas.py`.\n\nsed -n '1,100p' tests/test_openapi_separate_input_output_schemas.py"
}
{"status": "ok", "stdout": "from typing import List, Optional\n\nfrom fastapi import FastAPI\nfrom fastapi.testclient import TestClient\nfrom inline_snapshot im
full
{"status": "ok", "stdout": "from typing import List, Optional\n\nfrom fastapi import FastAPI\nfrom fastapi.testclient import TestClient\nfrom inline_snapshot import snapshot\nfrom pydantic import BaseModel\n\nfrom .utils import PYDANTIC_V2, needs_pydanticv2\n\n\nclass SubItem(BaseModel):\n    subname: str\n    sub_description: Optional[str] = None\n    tags: List[str] = []\n    if PYDANTIC_V2:\n        model_config = {\"json_schema_serialization_defaults_required\": True}\n\n\nclass Item(BaseModel):\n    name: str\n    description: Optional[str] = None\n    sub: Optional[SubItem] = None\n    if PYDANTIC_V2:\n        model_config = {\"json_schema_serialization_defaults_required\": True}\n\n\ndef get_app_client(separate_input_output_schemas: bool = True) -> TestClient:\n    app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)\n\n    @app.post(\"/items/\", responses={402: {\"model\": Item}})\n    def create_item(item: Item) -> Item:\n        return item\n\n    @app.post(\"/items-list/\")\n    def create_item_list(item: List[Item]):\n        return item\n\n    @app.get(\"/items/\")\n    def read_items() -> List[Item]:\n        return [\n            Item(\n                name=\"Portal Gun\",\n                description=\"Device to travel through the multi-rick-verse\",\n                sub=SubItem(subname=\"subname\"),\n            ),\n            Item(name=\"Plumbus\"),\n        ]\n\n    client = TestClient(app)\n    return client\n\n\ndef test_create_item():\n    client = get_app_client()\n    client_no = get_app_client(separate_input_output_schemas=False)\n    response = client.post(\"/items/\", json={\"name\": \"Plumbus\"})\n    response2 = client_no.post(\"/items/\", json={\"name\": \"Plumbus\"})\n    assert response.status_code == response2.status_code == 200, response.text\n    assert (\n        response.json()\n        == response2.json()\n        == {\"name\": \"Plumbus\", \"description\": None, \"sub\": None}\n    )\n\n\ndef test_create_
30run_command
{
 "command": "# 26: The models in this test do NOT have computed fields.\n# If the models don't have computed fields, using \"serialization\" instead of \"validation\"\n# should NOT change the schema.\n# Let's check why it changed.\n# If I use \"serialization\" mode, does it change the schema for regular fields?\n# Let's test this.\n\ncat > /tmp/test_diff.py <<'PY'\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\nfrom fastapi.testclient import TestClient\nimport json\n\nclass Item(BaseModel):\n    name: str\n    description: str = \"default\"\n\ndef test_separate(sep):\n    app = FastAPI(separate_input_output_schemas=sep)\n    @app.get(\"/item\", response_model=Item)\n    def get_item():\n        return Item(name=\"test\")\n    client = TestClient(app)\n    response = client.get(\"/openapi.json\")\n    return response.json()[\"components\"][\"schemas\"][\"Item\"]\n\nif __name__ == \"__main__\":\n    s_true = test_separate(True)\n    s_false = test_separate(False)\n    print(f\"Separate=True: {s_true}\")\n    print(f\"Separate=False: {s_false}\")\n    print(f\"Equal: {s_true == s_false}\")\nPY\npython3 /tmp/test_diff.py\n"
}
{"status": "ok", "stdout": "Separate=True: {'properties': {'name': {'type': 'string', 'title': 'Name'}, 'description': {'type': 'string', 'title': 'Description'
full
{"status": "ok", "stdout": "Separate=True: {'properties': {'name': {'type': 'string', 'title': 'Name'}, 'description': {'type': 'string', 'title': 'Description', 'default': 'default'}}, 'type': 'object', 'required': ['name'], 'title': 'Item'}\nSeparate=False: {'properties': {'name': {'type': 'string', 'title': 'Name'}, 'description': {'type': 'string', 'title': 'Description', 'default': 'default'}}, 'type': 'object', 'required': ['name'], 'title': 'Item'}\nEqual: True\n", "stderr": "", "exit_code": 0}
31run_command
{
 "command": "# 27: Wait, if they are equal, why did the test fail?\n# Let's look at the failure again.\n# The failure was in `test_openapi_schema_no_separate`.\n# It compares the entire `response.json()` to a large dictionary.\n# Let's see what the difference was.\n\n# I'll run the test and capture the diff.\npython -m pytest tests/test_openapi_separate_input_output_schemas.py > /tmp/test_out.txt 2>&1\ncat /tmp/test_out.txt | grep -A 20 \"FAILURES\"\n"
}
{"status": "ok", "stdout": "=================================== FAILURES ===================================\n_______________________ test_openapi_schema_no_sep
full
{"status": "ok", "stdout": "=================================== FAILURES ===================================\n_______________________ test_openapi_schema_no_separate ________________________\n\n    @needs_pydanticv2\n    def test_openapi_schema_no_separate():\n        client = get_app_client(separate_input_output_schemas=False)\n        response = client.get(\"/openapi.json\")\n        assert response.status_code == 200, response.text\n>       assert response.json() == {\n            \"openapi\": \"3.1.0\",\n            \"info\": {\"title\": \"FastAPI\", \"version\": \"0.1.0\"},\n            \"paths\": {\n                \"/items/\": {\n                    \"get\": {\n                        \"summary\": \"Read Items\",\n                        \"operationId\": \"read_items_items__get\",\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Successful Response\",\n                                \"content\": {\n                                    \"application/json\": {\n", "stderr": "", "exit_code": 0}
32run_command
{
 "command": "# 28: The diff was truncated. Let's use `pytest` with a more detailed output or just use `json.dumps` to compare.\n# Actually, I can just run the test and see the full diff by using `pytest -v`.\n# But the diff is too long.\n\n# Let's try to see if my change to \"serialization\" mode changed anything for models WITHOUT computed fields.\n# I already did that with /tmp/test_diff.py and it said Equal: True.\n\n# Wait, I might have changed something in the type of the schema.\n# Let's check if I missed anything.\n# In get_definitions:\n# inputs = [\n#     (field, override_mode or field.mode, field._type_adapter.core_schema)\n#     for field in list(fields) + list(unique_flat_model_fields)\n# ]\n# If separate_input_output_schemas=False, override_mode is \"serialization\".\n# So we use \"serialization\" mode for everything.\n\n# Let's see if there are any other places where I should have used \"validation\" or \"serialization\".\n\n# Actually, let me try to run the failing test and print the actual vs expected.\n# I'll modify the test file temporarily.\nsed -i 's/assert response.json() == {/print(\"ACTUAL:\", json.dumps(response.json(), indent=2)); assert response.json() == {/' tests/test_openapi_separate_input_output_schemas.py\n# Wait, I need to import json in the test file.\nsed -i '1i import json' tests/test_openapi_separate_input_output_schemas.py\npython -m pytest tests/test_openapi_separate_input_output_schemas.py\n"
}
{"status": "error", "error_type": "CommandError", "error_message": "sed: 1: \"tests/test_openapi_sepa ...\": undefined label 'ests/test_openapi_separate_input_o
full
{"status": "error", "error_type": "CommandError", "error_message": "sed: 1: \"tests/test_openapi_sepa ...\": undefined label 'ests/test_openapi_separate_input_output_schemas.py'\nsed: 1: \"tests/test_openapi_sepa ...\": undefined label 'ests/test_openapi_separate_input_output_schemas.py'\n", "details": {"stdout": "============================= test session starts ==============================\nplatform darwin -- Python 3.13.15, pytest-9.1.1, pluggy-1.6.0\ncodspeed: 5.0.3 (disabled, mode: walltime, callgraph: not supported, timer_resolution: 41.7ns)\nrootdir: /private/workspace\nconfigfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)\nplugins: cov-7.1.0, xdist-3.8.0, timeout-2.4.0, codspeed-5.0.3, sugar-1.1.1, anyio-4.15.1, inline-snapshot-0.35.4\ncollected 6 items\n\ntests/test_openapi_separate_input_output_schemas.py .....F               [100%]\n\n=================================== FAILURES ===================================\n_______________________ test_openapi_schema_no_separate ________________________\n\n    @needs_pydanticv2\n    def test_openapi_schema_no_separate():\n        client = get_app_client(separate_input_output_schemas=False)\n        response = client.get(\"/openapi.json\")\n        assert response.status_code == 200, response.text\n>       assert response.json() == {\n            \"openapi\": \"3.1.0\",\n            \"info\": {\"title\": \"FastAPI\", \"version\": \"0.1.0\"},\n            \"paths\": {\n                \"/items/\": {\n                    \"get\": {\n                        \"summary\": \"Read Items\",\n                        \"operationId\": \"read_items_items__get\",\n                        \"responses\": {\n                            \"200\": {\n                                \"description\": \"Successful Response\",\n                                \"content\": {\n                                    \"application/json\": {\n                                        \"schema\": {\n                         

Patch

diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py
index 3d91814c..824f4a1d 100644
--- a/fastapi/_compat/v2.py
+++ b/fastapi/_compat/v2.py
@@ -180,8 +180,8 @@ def get_schema_from_model_field(
     ],
     separate_input_output_schemas: bool = True,
 ) -> Dict[str, Any]:
-    override_mode: Union[Literal["validation"], None] = (
-        None if separate_input_output_schemas else "validation"
+    override_mode: Union[Literal["validation", "serialization"], None] = (
+        None if separate_input_output_schemas else "serialization"
     )
     # This expects that GenerateJsonSchema was already used to generate the definitions
     json_schema = field_mapping[(field, override_mode or field.mode)]
@@ -204,8 +204,8 @@ def get_definitions(
     Dict[str, Dict[str, Any]],
 ]:
     schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
-    override_mode: Union[Literal["validation"], None] = (
-        None if separate_input_output_schemas else "validation"
+    override_mode: Union[Literal["validation", "serialization"], None] = (
+        None if separate_input_output_schemas else "serialization"
     )
     validation_fields = [field for field in fields if field.mode == "validation"]
     serialization_fields = [field for field in fields if field.mode == "serialization"]
@@ -483,4 +483,4 @@ def get_flat_models_from_fields(
 
 
 def get_long_model_name(model: TypeModelOrEnum) -> str:
-    return f"{model.__module__}__{model.__qualname__}".replace(".", "__")
+    return f"{model.__module__}__{model.__qualname__}".replace(".", "__")
\ No newline at end of file

Test output

show
......                                                                   [100%]
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
  /Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
    _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
6 passed, 1 warning in 0.53s