File size: 7,161 Bytes
cd3b358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# FixFlow Sample Output β€” FastAPI Bug Analysis

**Issue:** [FastAPI response_model doesn't strip extra fields when using Pydantic v2](https://github.com/tiangolo/fastapi/issues/10876)  
**Repository:** https://github.com/tiangolo/fastapi  
**Analysis Time:** 87.3s  

---

## πŸ“‹ Step 1: Bug Summary

### πŸ› Error Message
> "When using `response_model` in FastAPI with Pydantic v2, extra fields defined in the response model are NOT stripped from the response. This breaks the behavior expected from the `response_model_exclude_unset` pattern."

### βœ… Expected Behavior
When a route has a `response_model` set, FastAPI should filter the response to only include fields defined in that model, stripping any additional fields from the underlying return value.

### ❌ Actual Behavior
Extra fields from the returned object are included in the JSON response even when a `response_model` is specified. This is a regression from Pydantic v1 behavior.

### πŸ” Reproduction Steps
1. Install `fastapi>=0.100.0` with `pydantic>=2.0.0`
2. Define a route: `@app.get("/users/{id}", response_model=UserOut)`
3. Return a `UserDB` object with extra fields not in `UserOut`
4. Observe: response includes the extra fields

### 🎯 Affected Components
- `fastapi/routing.py` β€” route handler serialization logic
- `fastapi/_compat.py` β€” Pydantic v1/v2 compatibility layer
- `fastapi/encoders.py` β€” JSON encoding pipeline

### πŸ” Key Technical Clues
- Introduced after Pydantic v2 migration
- `_get_value()` in `fastapi/_compat.py` changed behavior for model instances
- The `model_dump(exclude_unset=True)` call may not be filtering correctly

### πŸ’‘ Hypothesis
The Pydantic v2 compatibility layer in `_compat.py` is not correctly calling `model_dump()` with the `include`/`exclude` parameters that respect the `response_model` field constraints. The v2 migration changed how model field serialization works.

---

## πŸ” Step 2: Relevant Files

### πŸ“ Relevant Files (Ranked by Suspicion)

**1. `fastapi/_compat.py`**
- **Relevance score:** 10/10
- **Why relevant:** This is the Pydantic v1/v2 compatibility shim. All serialization changes went through here during the v2 migration.
- **What to look for:** `_get_value()`, `serialize_response()`, any calls to `model_dump()`

**2. `fastapi/routing.py`**
- **Relevance score:** 9/10  
- **Why relevant:** Contains `serialize_response()` calls that apply `response_model` filtering.
- **What to look for:** `get_request_handler()`, how `response_model_include` and `response_model_exclude` are passed.

**3. `fastapi/encoders.py`**
- **Relevance score:** 7/10
- **Why relevant:** `jsonable_encoder()` handles the final conversion to JSON-safe types.
- **What to look for:** Whether `include`/`exclude` sets are respected for Pydantic v2 models.

---

## πŸ”¬ Step 3: Root Cause Analysis

### Executive Summary
In `fastapi/_compat.py`, the `_get_value()` function for Pydantic v2 models calls `model_dump()` without passing the `include` parameter derived from the `response_model`'s field set, causing all fields to be serialized instead of only those defined in the response model.

### 🧠 Chain-of-Thought Reasoning

**Step 1: Entry Point**
A GET request hits a route decorated with `@app.get("/users/{id}", response_model=UserOut)`. FastAPI's `routing.py:get_request_handler()` is invoked, which calls `serialize_response()`.

**Step 2: Execution Trace**
- `routing.py:serialize_response()` β†’ calls `_compat.py:serialize_response()` with `response_model=UserOut`
- `_compat.py:serialize_response()` calls `_get_value(response, field=response_model_field, ...)`
- **Here's the bug:** For Pydantic v2, `_get_value()` calls `value.model_dump()` but does NOT pass `include=field_set` where `field_set` contains only the fields defined in `UserOut`

**Step 3: The Bug**
In `fastapi/_compat.py`, around line 215, the v2 branch of `_get_value()`:
```python
# BUGGY (current):
return value.model_dump(exclude_unset=exclude_unset, by_alias=by_alias)

# Should be:
return value.model_dump(
    include=include,
    exclude=exclude,
    exclude_unset=exclude_unset,
    by_alias=by_alias,
)
```
The `include` parameter (containing the `response_model`'s allowed fields) is accepted as a function argument but silently dropped in the v2 code path.

**Step 4: Why This Causes the Reported Behavior**
Without the `include` parameter, `model_dump()` serializes ALL fields of the returned object, bypassing the `response_model` restriction. In Pydantic v1, `_get_value()` used `dict()` which was correctly called with `include` β€” this broke during the v2 migration.

### πŸ“ Bug Location
- **File:** `fastapi/_compat.py`
- **Function/Class:** `_get_value()`
- **Line(s):** ~215
- **Type:** Missing parameter pass-through (regression from Pydantic v2 migration)

### 🎯 Confidence Level
**High** β€” The bug is clearly a missing parameter in a well-understood code path. The fix is straightforward and surgical.

---

## πŸ”§ Step 4: Proposed Fix

```diff
--- a/fastapi/_compat.py
+++ b/fastapi/_compat.py
@@ -212,7 +212,11 @@ def _get_value(
         if PYDANTIC_V2:
             if isinstance(value, BaseModel):
-                return value.model_dump(exclude_unset=exclude_unset, by_alias=by_alias)
+                # Pass include/exclude to respect response_model field constraints
+                # This was missing after the Pydantic v2 migration (regression fix)
+                return value.model_dump(
+                    include=include,
+                    exclude=exclude,
+                    exclude_unset=exclude_unset,
+                    by_alias=by_alias,
+                )
```

---

## πŸ“ Step 5: PR Description

### πŸ“ Pull Request: Fix response_model field filtering with Pydantic v2

#### πŸ› Problem
When using FastAPI with Pydantic v2, the `response_model` parameter on route decorators no longer strips extra fields from responses. A route returning a `UserDB` object (with password, internal fields) but declaring `response_model=UserOut` would incorrectly expose the extra fields to clients.

#### πŸ” Root Cause
During the Pydantic v2 migration, `fastapi/_compat.py`'s `_get_value()` function lost the `include` parameter pass-through in the v2 code path. The `model_dump()` call was not forwarding the field inclusion constraints derived from the `response_model`.

#### πŸ”§ Solution
Added `include=include` and `exclude=exclude` parameters to the `model_dump()` call in the Pydantic v2 branch of `_get_value()`. This restores the Pydantic v1 behavior where only `response_model` fields are serialized.

#### πŸ§ͺ Testing Recommendations
1. Create a route returning an object with extra fields, verify response only includes `response_model` fields
2. Test `response_model_exclude_unset=True` still works correctly
3. Run existing test suite: `pytest tests/test_response_model.py -v`

#### ⚠️ Potential Side Effects
None identified. Change only affects the Pydantic v2 code path and is additive β€” it passes parameters that were already being constructed but not forwarded.

---

*Generated by FixFlow β€” Autonomous Bug Resolution Agent powered by GLM 5.1 (Z.ai)*