Skip to content

Commit 08210a4

Browse files
authored
fix: prevent serialization from emitting pydantic serializer warnings (#95)
1 parent a1fae25 commit 08210a4

3 files changed

Lines changed: 67 additions & 2 deletions

File tree

py/src/braintrust/bt_json.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dataclasses
22
import json
33
import math
4+
import warnings
45
from typing import Any, Callable, Mapping, NamedTuple, cast, overload
56

67

@@ -53,8 +54,13 @@ def _to_bt_safe(v: Any) -> Any:
5354
pass
5455

5556
# Attempt to dump a Pydantic v2 `BaseModel`.
57+
# Suppress Pydantic serializer warnings that arise from generic/discriminated-union
58+
# models (e.g. OpenAI's ParsedResponse[T]). See
59+
# https://github.com/braintrustdata/braintrust-sdk-python/issues/60
5660
try:
57-
return cast(Any, v).model_dump(exclude_none=True)
61+
with warnings.catch_warnings():
62+
warnings.filterwarnings("ignore", message="Pydantic serializer warnings", category=UserWarning)
63+
return cast(Any, v).model_dump(exclude_none=True)
5864
except (AttributeError, TypeError):
5965
pass
6066

py/src/braintrust/oai.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import base64
33
import re
44
import time
5+
import warnings
56
from collections.abc import Callable
67
from typing import Any
78

@@ -1023,9 +1024,14 @@ def _try_to_dict(obj: Any) -> dict[str, Any]:
10231024
if isinstance(obj, dict):
10241025
return obj
10251026
# convert a pydantic object to a dict
1027+
# Suppress Pydantic serializer warnings from generic/discriminated-union models
1028+
# (e.g. OpenAI's ParsedResponse[T]). See
1029+
# https://github.com/braintrustdata/braintrust-sdk-python/issues/60
10261030
if hasattr(obj, "model_dump") and callable(obj.model_dump):
10271031
try:
1028-
return obj.model_dump()
1032+
with warnings.catch_warnings():
1033+
warnings.filterwarnings("ignore", message="Pydantic serializer warnings", category=UserWarning)
1034+
return obj.model_dump()
10291035
except Exception:
10301036
pass
10311037
# deprecated pydantic method, try model_dump first.

py/src/braintrust/test_bt_json.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# pyright: reportUnknownArgumentType=false
33
# pyright: reportPrivateUsage=false
44
import json
5+
import warnings
56
from typing import Any
67
from unittest import TestCase
78

@@ -282,6 +283,58 @@ def test_deep_copy_numeric_and_special_keys(self):
282283
self.assertTrue("(1, 2)" in result or "1, 2" in result)
283284
self.assertIn("None", result)
284285

286+
def test_model_dump_no_pydantic_serializer_warnings(self):
287+
"""Test that model_dump() calls in _to_bt_safe don't emit Pydantic serializer warnings.
288+
289+
Regression test for https://github.com/braintrustdata/braintrust-sdk-python/issues/60
290+
OpenAI's parsed response models (e.g. ParsedResponse[T]) emit
291+
PydanticSerializationUnexpectedValue warnings from model_dump().
292+
Braintrust should suppress these warnings during serialization.
293+
"""
294+
try:
295+
from pydantic import BaseModel
296+
except ImportError:
297+
self.skipTest("Pydantic not available")
298+
299+
class WarningModel(BaseModel):
300+
"""A model whose model_dump() emits a UserWarning, simulating OpenAI parsed responses."""
301+
302+
name: str
303+
304+
def model_dump(self, **kwargs):
305+
warnings.warn(
306+
"Pydantic serializer warnings:\n PydanticSerializationUnexpectedValue: Expected `none`",
307+
UserWarning,
308+
stacklevel=2,
309+
)
310+
return {"name": self.name}
311+
312+
obj = WarningModel(name="test")
313+
314+
# _to_bt_safe (via bt_safe_deep_copy) should NOT propagate the warning
315+
with warnings.catch_warnings(record=True) as caught:
316+
warnings.simplefilter("always")
317+
result = bt_safe_deep_copy({"response": obj})
318+
pydantic_warnings = [w for w in caught if "Pydantic serializer warnings" in str(w.message)]
319+
assert len(pydantic_warnings) == 0, (
320+
f"Expected no Pydantic serializer warnings, but got {len(pydantic_warnings)}: "
321+
f"{[str(w.message) for w in pydantic_warnings]}"
322+
)
323+
324+
assert result["response"]["name"] == "test"
325+
326+
# bt_dumps should also NOT propagate the warning
327+
with warnings.catch_warnings(record=True) as caught:
328+
warnings.simplefilter("always")
329+
json_str = bt_dumps(obj)
330+
pydantic_warnings = [w for w in caught if "Pydantic serializer warnings" in str(w.message)]
331+
assert len(pydantic_warnings) == 0, (
332+
f"Expected no Pydantic serializer warnings, but got {len(pydantic_warnings)}: "
333+
f"{[str(w.message) for w in pydantic_warnings]}"
334+
)
335+
336+
assert "test" in json_str
337+
285338

286339
@pytest.mark.vcr
287340
def test_to_bt_safe_special_objects():

0 commit comments

Comments
 (0)