← baseline_full

fastapi_14463

failed NO_PATCH NO_PATCH UNSUBMITTED empty_patch(None) · None tool calls · 0 s · fastapi/fastapi

Task input

🐛 Fix OpenAPI duplication of `anyOf` refs for app-level responses with specified `content` and `model` as `Union`

## Summary

Fixes an issue where defining app-level `responses={...}` with a `Union` of Pydantic models **and** specifying `["content"][...]["examples"]` causes the generated OpenAPI schema to accumulate duplicate `$ref` entries in the `anyOf` array.



## Minimal reproducible example

<details open>
<summary> <code>app.py</code> </summary>

```python
from fastapi import FastAPI
from pydantic import BaseModel


class ModelA(BaseModel):
    a: str


class ModelB(BaseModel):
    b: str


app = FastAPI(
    responses={
        500: {
            'model': ModelA | ModelB,
            'content': {"application/json": {
                'examples': {"Case A": {"value": "a"}}
            }}
        }
    }
)


@app.get('/route1')
def route1():
    return "test"

@app.get('/route2')
def route2():
    return "test"

@app.get('/route3')
def route2():
    return "test"
```
</details>

<details open>
<summary>Generated <code>/openapi.json</code> excerpt (<code>paths["/route1"]["get"]["responses"]</code>)</summary>

```json
{
  "500": {
    "description": "Internal Server Error",
    "content": {
      "application/json": {
        "schema": {
          "anyOf": [
            { "$ref": "#/components/schemas/ModelA" },
            { "$ref": "#/components/schemas/ModelB" },
            { "$ref": "#/components/schemas/ModelA" },
            { "$ref": "#/components/schemas/ModelB" },
            { "$ref": "#/components/schemas/ModelA" },
            { "$ref": "#/components/schemas/ModelB" }
          ]
        }
       }
     }
  }
}
```
</details>

As you can see, the `$ref`s are duplicated three times within `anyOf`. 

This appears to be an unintended behavior, since FastAPI’s documentation does not indicate that specifying both a `model` and `content` is prohibited. The generated schema is still valid, but the duplication grows with teh route count and produces excessively large `anyOf` lists for bigger applications.


<details open>
<summary> <h2>Root Cause</h2> </summary>

[Relevant Code Snippet](https://github.com/fastapi/fastapi/blob/f0dd1046a688935ffd23666b3d4164b838a4d8fe/fastapi/openapi/utils.py#L376-L416)

First, `additional_response` is taken from the `route` object:

https://github.com/fastapi/fastapi/blob/f0dd1046a688935ffd23666b3d4164b838a4d8fe/fastapi/openapi/utils.py#L376-L379

<details>
<summary> Example <code>additional_response</code> </summary>

```python
{
    'model': ModelA | ModelB,
    'content': {"application/json": {
        'examples': {"Case A": {"value": "a"}}
    }}
}
```
</details>

 `additional_response` is *shallowly* `.copy()`ed into `process_response`:

https://github.com/fastapi/fastapi/blob/f0dd1046a688935ffd23666b3d4164b838a4d8fe/fastapi/openapi/utils.py#L380

Now, `id(process_response['content']) == id(additional_response['content'])`, so when `process_response['content']` is modified, the `route.responses` object inadvertently modified as well. And, since the response was defined within `FastAPI(...)`, all the routes share the same `route.responses` object.

https://github.com/fastapi/fastapi/blob/f0dd1046a688935ffd23666b3d4164b838a4d8fe/fastapi/openapi/utils.py#L401-L406

`deep_dict_update()` will combine `anyOf` segments together:

https://github.com/fastapi/fastapi/blob/f0dd1046a688935ffd23666b3d4164b838a4d8fe/fastapi/utils.py#L227-L242

When this method is called the second time (for the second route), `additional_response` will now already contain the schema definition from the first route:

<details>
<summary> Example <code>additional_response</code> </summary>

```python
{
  'model': ModelA | ModelB,
  'content': {"application/json": {
      'examples': {"Case A": {"value": "a"}}
  }},
  'schema": {
    "anyOf": [
      { "$ref": "#/components/schemas/ModelA" },
      { "$ref": "#/components/schemas/ModelB" }
    ]
  }
}
```
</details>

And, for each subsequent route, the additional "anyOf" elements will keep appending to the same array.

</details>

<details open>
<summary> <h2> Workaround (before fix) </h2> </summary>

Define the schema manually instead of using `model`:

```python
app = FastAPI(
    responses={
        500: {
            'content': {"application/json": {
                'examples': {"Case A": {"value": "a"}},
                'schema': {
                    "anyOf": [
                        model.model_json_schema() for model in [ModelA, ModelB]
                    ]
                }
            }}
        }
    }
)
```

</details>

## Additional Information
<details>
<summary> Full <code>openapi.json</code> </summary>

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "paths": {
    "/route1": {
      "get": {
        "summary": "Route1",
        "operationId": "route1_route1_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ModelA"
                    },
                    {
                      "$ref": "#/components/schemas/ModelB"
                    },
                    {
                      "$ref": "#/components/schemas/ModelA"
                    },
                    {
                      "$ref": "#/components/schemas/ModelB"
                    },
                    {
                      "$ref": "#/components/schemas/ModelA"
                    },
                    {
                      "$ref": "#/components/schemas/ModelB"
                    }
                  ],
                  "title": "Response 500 Route3 Route3 Get"
                },
                "examples": {
                  "Case A": {
                    "value": "a"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/route2": {
      "get": {
        "summary": "Route2",
        "operationId": "route2_route2_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ModelA"
                    },
                    {
                      "$ref": "#/components/schemas/ModelB"
                    },
                    {
                      "$ref": "#/components/schemas/ModelA"
                    },
                    {
                      "$ref": "#/components/schemas/ModelB"
                    },
                    {
                      "$ref": "#/components/schemas/ModelA"
                    },
                    {
                      "$ref": "#/components/schemas/ModelB"
                    }
                  ],
                  "title": "Response 500 Route3 Route3 Get"
                },
                "examples": {
                  "Case A": {
                    "value": "a"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/route3": {
      "get": {
        "summary": "Route3",
        "operationId": "route3_route3_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {

                }
              }
            }
          },
          "500": {
            "description": "Internal Server Error",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ModelA"
                    },
                    {
                      "$ref": "#/components/schemas/ModelB"
                    },
                    {
                      "$ref": "#/components/schemas/ModelA"
                    },
                    {
                      "$ref": "#/components/schemas/ModelB"
                    },
                    {
                      "$ref": "#/components/schemas/ModelA"
                    },
                    {
                      "$ref": "#/components/schemas/ModelB"
                    }
                  ],
                  "title": "Response 500 Route3 Route3 Get"
                },
                "examples": {
                  "Case A": {
                    "value": "a"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ModelA": {
        "properties": {
          "a": {
            "type": "string",
            "title": "A"
          }
        },
        "type": "object",
        "required": [
          "a"
        ],
        "title": "ModelA"
      },
      "ModelB": {
        "properties": {
          "b": {
            "type": "string",
            "title": "B"
          }
        },
        "type": "object",
        "required": [
          "b"
        ],
        "title": "ModelB"
      }
    }
  }
}
```

</details>

Tool calls (0)

#ToolArgumentsResult
No trace captured.