CodeSampleX

샘플

fastapi 0.141.1: Exclude default values in Pydantic models nested in dict keys and values when serializing with jsonable_encoder as fixed in FastAPI 0.140.9

검증된 샘플 — pypi fastapi 0.141.1: Exclude default values in Pydantic models nested in dict keys and values when serializing with jsonable_encoder as fixed in…

sha256:b09b062b12f13c860852641882840dceca140b65e173f95222f347f4f95b25b6

이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다. 통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다. MIT-0

실행 증거

선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.

증거 기준
서명된 컨트랙트 통과
검증 영수증
2
빌드한 서명 키
2
선언된 환경 python linux x64 python python pip

검증 실행 환경

환경 컨트랙트 단계 실행일
python 3.12 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-16
python 3.12 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1
2026-08-18

케이스

HOW
목표
Exclude default values in Pydantic models nested in dict keys and values when serializing with jsonable_encoder as fixed in FastAPI 0.140.9
패키지
심벌
  • fastapi.encoders.jsonable_encoder
  • fastapi.FastAPI
  • fastapi.testclient.TestClient
환경
python
생성일
2026-08-16T06:22:41Z

컨트랙트

  1. assert TestClient targets http://testserver, so the contract runs with network off and no DNS sockets
  2. assert jsonable_encoder([item], exclude_defaults=True) excludes default fields from items in top-level lists
  3. assert jsonable_encoder({'item': item}, exclude_defaults=True) propagates exclude_defaults into dict values instead of leaking default fields
  4. assert jsonable_encoder({'items': [item]}, exclude_defaults=True) propagates exclude_defaults through dicts containing lists
  5. assert jsonable_encoder({'data': {'items': [item]}}, exclude_defaults=True) propagates exclude_defaults through deeply nested dicts
  6. assert jsonable_encoder preserves default fields when exclude_defaults is False or omitted
  7. assert jsonable_encoder preserves non-default overridden fields when exclude_defaults is True
  8. assert exclude_defaults=True strips fields explicitly assigned their default value whereas exclude_unset=True retains them
  9. assert jsonable_encoder excludes defaults from nested Pydantic container models inside dictionaries
  10. assert jsonable_encoder({'data': null_model}, exclude_none=True) excludes None fields consistently
  11. assert FastAPI route responses returning dicts with exclude_defaults=True omit default fields over the wire

파일

  • NOTES.md
  • csx.json
  • requirements.txt
  • src/__init__.py
  • src/app.py
  • test/contract.py

소스 아티팩트 내려받기 (tar.gz)

소스

NOTES.md
# Notes: FastAPI jsonable_encoder exclude_defaults Dictionary Propagation Bugfix

## 1. Network Search Result
`search_known_solution` returned existing sample `sha256:8bcaa6f98cea6edf681d2b0f76a18856106de62aab70009b1d22a5a50abb1257` (*"Test a FastAPI app with TestClient and no network, and assert the validation error bodies it returns"*). That sample covers general endpoint validation error structures rather than patch release bugfixes.

## 2. Release and Notes Location
- **Release**: `fastapi 0.140.9` (Released July 28, 2026)
- **Source**: FastAPI GitHub Releases / Release Notes under `0.140.9` -> `### Fixes`:
  > `* 🐛 Fix exclude_defaults not propagated to dict keys and values in jsonable_encoder. PR #16043 by @MBGrao.`
- **Pull Request**: PR [#16043](https://github.com/fastapi/fastapi/pull/16043) (*"Fix `exclude_defaults` not propagated to dict keys and values in `jsonable_encoder`"*)

## 3. What a Model Would Have Written Instead
A model would assume that passing `exclude_defaults=True` to `jsonable_encoder` (or returning dictionary structures containing Pydantic models from route handlers) recursively strips default fields uniformly across all container types (lists, tuples, sets, and mappings).

## 4. How the Wrong Version Fails
In FastAPI versions prior to `0.140.9`, the wrong version fails **silently with a green build**: `jsonable_encoder([model], exclude_defaults=True)` correctly omitted default fields, but `jsonable_encoder({"item": model}, exclude_defaults=True)` and `jsonable_encoder({"items": [model]}, exclude_defaults=True)` silently retained all default values because `fastapi/encoders.py` omitted `exclude_defaults` during dictionary key and value recursion, leaking default fields into serialization payloads and HTTP responses.
csx.json
{"case":{"caseId":"case:sha256:a0061f9c2f248c9f7f4447ca5f2bd5c4aa3b888a61066f2d9caa529ae81035a8","contract":["assert TestClient targets http://testserver, so the contract runs with network off and no DNS sockets","assert jsonable_encoder([item], exclude_defaults=True) excludes default fields from items in top-level lists","assert jsonable_encoder({'item': item}, exclude_defaults=True) propagates exclude_defaults into dict values instead of leaking default fields","assert jsonable_encoder({'items': [item]}, exclude_defaults=True) propagates exclude_defaults through dicts containing lists","assert jsonable_encoder({'data': {'items': [item]}}, exclude_defaults=True) propagates exclude_defaults through deeply nested dicts","assert jsonable_encoder preserves default fields when exclude_defaults is False or omitted","assert jsonable_encoder preserves non-default overridden fields when exclude_defaults is True","assert exclude_defaults=True strips fields explicitly assigned their default value whereas exclude_unset=True retains them","assert jsonable_encoder excludes defaults from nested Pydantic container models inside dictionaries","assert jsonable_encoder({'data': null_model}, exclude_none=True) excludes None fields consistently","assert FastAPI route responses returning dicts with exclude_defaults=True omit default fields over the wire"],"goal":"Exclude default values in Pydantic models nested in dict keys and values when serializing with jsonable_encoder as fixed in FastAPI 0.140.9","kind":"HOW","packages":["pkg:pypi/fastapi@0.141.1","pkg:pypi/pydantic@2.13.4","pkg:pypi/starlette@1.6.0","pkg:pypi/httpx@0.28.1"],"schemaVersion":1,"symbols":["fastapi.encoders.jsonable_encoder","fastapi.FastAPI","fastapi.testclient.TestClient"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","ecosystem":"pypi","executionContext":"python","language":"python","os":"linux","packageManager":"pip","runtime":"python","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/fastapi@0.141.1","pkg:pypi/pydantic@2.13.4","pkg:pypi/starlette@1.6.0","pkg:pypi/httpx@0.28.1"],"schemaVersion":1,"symbols":["fastapi.encoders.jsonable_encoder","fastapi.FastAPI","fastapi.testclient.TestClient"],"verifierAdapter":"python@1"}
requirements.txt
fastapi==0.141.1
starlette==1.6.0
annotated-doc==0.0.5
pydantic==2.13.4
pydantic-core==2.46.4
typing-extensions==4.16.0
typing-inspection==0.4.4
annotated-types==0.8.0
anyio==4.14.2
idna==3.18
httpx==0.28.1
httpcore==1.0.9
h11==0.16.0
certifi==2026.7.22
src/__init__.py
"""Package root for FastAPI bugfix verification sample."""
src/app.py
"""FastAPI application demonstrating jsonable_encoder default-value exclusion behavior.

FastAPI 0.140.9 fixed a bug (PR #16043) where `jsonable_encoder` forwarded `exclude_defaults=True`
when recursing into lists, tuples, and sets, but silently dropped `exclude_defaults` when
recursing into dictionary keys and values.

In versions prior to 0.140.9:
- `jsonable_encoder([model], exclude_defaults=True)` stripped default fields as expected.
- `jsonable_encoder({"item": model}, exclude_defaults=True)` silently kept all default fields.
- `jsonable_encoder({"items": [model]}, exclude_defaults=True)` silently kept all default fields
  because the outer dictionary recursion dropped `exclude_defaults` before reaching the inner list.

In FastAPI >= 0.140.9, dictionary recursion correctly propagates `exclude_defaults=exclude_defaults`,
producing consistent default exclusion across all container shapes.
"""

from typing import Any, Dict, List
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field


class Item(BaseModel):
    """Pydantic model with default values, default_factory, and required fields."""
    name: str = "default_name"
    tag: str = "default_tag"
    custom: str
    generated: List[int] = Field(default_factory=lambda: [1, 2, 3])


class Container(BaseModel):
    """Container model nesting an Item."""
    label: str = "default_label"
    item: Item


app = FastAPI(title="FastAPI jsonable_encoder bugfix verification")


@app.get("/items/list")
def get_items_list() -> JSONResponse:
    """Returns a list of items encoded with exclude_defaults=True."""
    item = Item(custom="custom_val")
    encoded = jsonable_encoder([item], exclude_defaults=True)
    return JSONResponse(content=encoded)


@app.get("/items/dict")
def get_items_dict() -> JSONResponse:
    """Returns a dictionary containing an item encoded with exclude_defaults=True."""
    item = Item(custom="custom_val")
    encoded = jsonable_encoder({"item": item}, exclude_defaults=True)
    return JSONResponse(content=encoded)


@app.get("/items/nested")
def get_items_nested() -> JSONResponse:
    """Returns a nested dictionary with a list of items encoded with exclude_defaults=True."""
    item = Item(custom="custom_val")
    encoded = jsonable_encoder({"data": {"items": [item]}}, exclude_defaults=True)
    return JSONResponse(content=encoded)


@app.get("/items/unset-vs-defaults")
def get_unset_vs_defaults() -> Dict[str, Any]:
    """Demonstrates difference between exclude_unset and exclude_defaults in dicts."""
    # When a field is explicitly assigned its default value:
    # - exclude_unset=True KEEPS the field (it was explicitly set)
    # - exclude_defaults=True STRIPS the field (its value matches the default)
    item_explicit_default = Item(name="default_name", custom="custom_val")
    return {
        "unset_excluded": jsonable_encoder(
            {"item": item_explicit_default}, exclude_unset=True
        ),
        "defaults_excluded": jsonable_encoder(
            {"item": item_explicit_default}, exclude_defaults=True
        ),
    }
test/contract.py
import sys
from pathlib import Path

# Add project root to sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

import fastapi
from fastapi.encoders import jsonable_encoder
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

from src.app import Container, Item, app

# --- 1. TestClient runs in-process with no network or DNS sockets --------
client = TestClient(app)
assert str(client.base_url) == "http://testserver"

# --- 2. jsonable_encoder: exclude_defaults propagation to dicts -----------
# In FastAPI < 0.140.9, jsonable_encoder passed exclude_defaults when recursing
# into lists/tuples/sets, but dropped it when recursing into dict keys/values.
# In FastAPI >= 0.140.9 (PR #16043), exclude_defaults propagates across all containers.
item = Item(custom="custom_val")

# Lists worked before and continue to work:
encoded_list = jsonable_encoder([item], exclude_defaults=True)
assert encoded_list == [{"custom": "custom_val"}]

# Dictionaries previously failed (leaking default_name, default_tag, generated=[1, 2, 3]):
encoded_dict = jsonable_encoder({"item": item}, exclude_defaults=True)
assert encoded_dict == {"item": {"custom": "custom_val"}}, (
    f"Expected defaults excluded in dict value, got: {encoded_dict}"
)

# Lists nested inside dictionaries also previously failed because the outer dict dropped the flag:
encoded_dict_list = jsonable_encoder({"items": [item]}, exclude_defaults=True)
assert encoded_dict_list == {"items": [{"custom": "custom_val"}]}, (
    f"Expected defaults excluded in dict-nested list, got: {encoded_dict_list}"
)

# Deeply nested dictionaries:
encoded_deep = jsonable_encoder({"data": {"items": [item]}}, exclude_defaults=True)
assert encoded_deep == {"data": {"items": [{"custom": "custom_val"}]}}, (
    f"Expected defaults excluded in deeply nested dict, got: {encoded_deep}"
)

# --- 3. Default behavior preserves all default fields --------------------
encoded_default = jsonable_encoder({"item": item})
assert encoded_default == {
    "item": {
        "name": "default_name",
        "tag": "default_tag",
        "custom": "custom_val",
        "generated": [1, 2, 3],
    }
}
assert jsonable_encoder({"item": item}, exclude_defaults=False) == encoded_default

# --- 4. Overridden non-default fields are preserved -----------------------
item_override = Item(name="custom_name", custom="custom_val")
encoded_override = jsonable_encoder({"item": item_override}, exclude_defaults=True)
assert encoded_override == {"item": {"name": "custom_name", "custom": "custom_val"}}

# --- 5. Distinction between exclude_defaults and exclude_unset in dicts ---
# When a field is explicitly initialized to a value equal to its default:
# - exclude_unset=True keeps the field (it was explicitly set during instantiation)
# - exclude_defaults=True excludes the field (its value matches the model's default)
item_explicit_default = Item(name="default_name", custom="custom_val")

encoded_unset = jsonable_encoder(
    {"item": item_explicit_default}, exclude_unset=True
)
assert encoded_unset == {
    "item": {"name": "default_name", "custom": "custom_val"}
}

encoded_defaults = jsonable_encoder(
    {"item": item_explicit_default}, exclude_defaults=True
)
assert encoded_defaults == {"item": {"custom": "custom_val"}}

# --- 6. Nested Pydantic container model serialization ---------------------
container = Container(item=item)
# Top-level model has label="default_label" (stripped) and nested item (defaults stripped)
encoded_container = jsonable_encoder(container, exclude_defaults=True)
assert encoded_container == {"item": {"custom": "custom_val"}}

# Container inside a dictionary
encoded_container_dict = jsonable_encoder(
    {"container": container}, exclude_defaults=True
)
assert encoded_container_dict == {"container": {"item": {"custom": "custom_val"}}}

# --- 7. Sibling flag exclude_none operates consistently ------------------
class NullableModel(BaseModel):
    required_val: str
    optional_val: str | None = None

null_model = NullableModel(required_val="hello", optional_val=None)
encoded_none = jsonable_encoder({"data": null_model}, exclude_none=True)
assert encoded_none == {"data": {"required_val": "hello"}}

# --- 8. Endpoint responses via TestClient --------------------------------
res_list = client.get("/items/list")
assert res_list.status_code == 200
assert res_list.json() == [{"custom": "custom_val"}]

res_dict = client.get("/items/dict")
assert res_dict.status_code == 200
assert res_dict.json() == {"item": {"custom": "custom_val"}}

res_nested = client.get("/items/nested")
assert res_nested.status_code == 200
assert res_nested.json() == {"data": {"items": [{"custom": "custom_val"}]}}

res_comparison = client.get("/items/unset-vs-defaults")
assert res_comparison.status_code == 200
assert res_comparison.json() == {
    "unset_excluded": {"item": {"name": "default_name", "custom": "custom_val"}},
    "defaults_excluded": {"item": {"custom": "custom_val"}},
}

print(
    f"contract ok: fastapi {fastapi.__version__} - "
    "exclude_defaults correctly propagated to dict keys and values in jsonable_encoder"
)

오리진 시더

csx-seed