From ccc9f17c396b9972f38e72a491aa4ef4b79566a4 Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Sun, 8 Mar 2026 02:37:33 +0100 Subject: [PATCH 1/2] fix: omit `by_alias` from `model_dump` kwargs when `None` to prevent TypeError `model_dump()` in `_compat.py` declares `by_alias: bool | None = None` but passes the value directly to Pydantic v2's `model_dump()`, which expects a `bool`. When `by_alias` is `None` (the default), pydantic-core's Rust serializer raises `TypeError: argument 'by_alias': 'NoneType' object cannot be converted to 'PyBool'`. This only manifests when DEBUG logging is enabled, because the affected call path is inside `_build_request()`'s `log.isEnabledFor(logging.DEBUG)` block. Omit `by_alias` from kwargs when `None` so Pydantic uses its model-level default (preserving tri-state semantics for models with `ConfigDict(serialize_by_alias=True)`), and only forward explicit `True`/`False` values. Fixes #2921 Signed-off-by: Giulio Leone <6887247+giulio-leone@users.noreply.github.com> --- src/openai/_compat.py | 6 ++++-- tests/test_models.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/openai/_compat.py b/src/openai/_compat.py index 020ffeb2ca..530097a3ad 100644 --- a/src/openai/_compat.py +++ b/src/openai/_compat.py @@ -142,15 +142,17 @@ def model_dump( by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): - return model.model_dump( + kwargs: dict[str, Any] = dict( mode=mode, exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, - by_alias=by_alias, ) + if by_alias is not None: + kwargs["by_alias"] = by_alias + return model.model_dump(**kwargs) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] diff --git a/tests/test_models.py b/tests/test_models.py index 588869ee35..05db6a85b1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -157,6 +157,20 @@ def test_unknown_fields() -> None: assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}} +def test_model_dump_by_alias_none() -> None: + """Ensure model_dump does not crash when by_alias defaults to None (GH-2921).""" + m = BasicModel.construct(foo="hello") + # by_alias=None is the default — must not raise TypeError + result = model_dump(m) + assert result == {"foo": "hello"} + # Explicit by_alias=None must also work + result = model_dump(m, by_alias=None) + assert result == {"foo": "hello"} + # Explicit by_alias=True/False must still be forwarded + result = model_dump(m, by_alias=False) + assert result == {"foo": "hello"} + + def test_strict_validation_unknown_fields() -> None: class Model(BaseModel): foo: str From 7e643215b59c6ab9eba379e0610494811808cbce Mon Sep 17 00:00:00 2001 From: giulio-leone Date: Mon, 9 Mar 2026 19:31:48 +0100 Subject: [PATCH 2/2] fix: preserve by_alias tri-state semantics in Pydantic v1 path Only coerce to bool when by_alias is not None, avoiding loss of None vs False distinction in the v1 fallback code path. --- src/openai/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openai/_compat.py b/src/openai/_compat.py index 530097a3ad..286b5be3f7 100644 --- a/src/openai/_compat.py +++ b/src/openai/_compat.py @@ -156,7 +156,7 @@ def model_dump( return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) if by_alias is not None else by_alias ), )