Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix serialization of pydantic.BaseModel fields with pathlib.Path dict keys #1157

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 10 additions & 17 deletions python/langsmith/_internal/_serde.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
import pathlib
import re
import uuid
from typing import (
Any,
)
from typing import Any

import orjson

Expand All @@ -33,14 +31,8 @@ def _simple_default(obj):
# https://github.com/ijl/orjson#serialize
if isinstance(obj, datetime.datetime):
return obj.isoformat()
if isinstance(obj, uuid.UUID):
elif isinstance(obj, uuid.UUID):
return str(obj)
if hasattr(obj, "model_dump") and callable(obj.model_dump):
return obj.model_dump()
elif hasattr(obj, "dict") and callable(obj.dict):
return obj.dict()
elif hasattr(obj, "_asdict") and callable(obj._asdict):
return obj._asdict()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed these are they are dead code - namedtuple, and pydantic.BaseModel are handled in _serialize_json

elif isinstance(obj, BaseException):
return {"error": type(obj).__name__, "message": str(obj)}
elif isinstance(obj, (set, frozenset, collections.deque)):
Expand Down Expand Up @@ -86,21 +78,22 @@ def _serialize_json(obj: Any) -> Any:
return list(obj)

serialization_methods = [
("model_dump", True), # Pydantic V2 with non-serializable fields
("dict", False), # Pydantic V1 with non-serializable field
("to_dict", False), # dataclasses-json
(
"model_dump",
{"exclude_none": True, "mode": "json"},
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mode="json" convert pathlib.Path dict keys to str

), # Pydantic V2 with non-serializable fields
("dict", {}), # Pydantic V1 with non-serializable field
("to_dict", {}), # dataclasses-json
]
for attr, exclude_none in serialization_methods:
for attr, kwargs in serialization_methods:
if (
hasattr(obj, attr)
and callable(getattr(obj, attr))
and not isinstance(obj, type)
):
try:
method = getattr(obj, attr)
response = (
method(exclude_none=exclude_none) if exclude_none else method()
)
response = method(**kwargs)
if not isinstance(response, dict):
return str(response)
return response
Expand Down
26 changes: 23 additions & 3 deletions python/tests/unit_tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import logging
import math
import pathlib
import sys
import time
import uuid
Expand Down Expand Up @@ -719,16 +720,24 @@ def test_pydantic_serialize() -> None:

class ChildPydantic(BaseModel):
uid: uuid.UUID
child_path_keys: Dict[pathlib.Path, pathlib.Path]

class MyPydantic(BaseModel):
foo: str
uid: uuid.UUID
tim: datetime
ex: Optional[str] = None
child: Optional[ChildPydantic] = None
path_keys: Dict[pathlib.Path, pathlib.Path]

obj = MyPydantic(
foo="bar", uid=test_uuid, tim=test_time, child=ChildPydantic(uid=test_uuid)
foo="bar",
uid=test_uuid,
tim=test_time,
child=ChildPydantic(
uid=test_uuid, child_path_keys={pathlib.Path("foo"): pathlib.Path("bar")}
),
path_keys={pathlib.Path("foo"): pathlib.Path("bar")},
)
res = json.loads(json.dumps(obj, default=_serialize_json))
expected = {
Expand All @@ -737,7 +746,9 @@ class MyPydantic(BaseModel):
"tim": test_time.isoformat(),
"child": {
"uid": str(test_uuid),
"child_path_keys": {"foo": "bar"},
},
"path_keys": {"foo": "bar"},
}
assert res == expected

Expand Down Expand Up @@ -777,6 +788,7 @@ def __repr__(self):
class MyPydantic(BaseModel):
foo: str
bar: int
path_keys: Dict[pathlib.Path, "MyPydantic"]

@dataclasses.dataclass
class MyDataclass:
Expand Down Expand Up @@ -816,7 +828,11 @@ class MyNamedTuple(NamedTuple):
"class_with_tee": ClassWithTee(),
"my_dataclass": MyDataclass("foo", 1),
"my_enum": MyEnum.FOO,
"my_pydantic": MyPydantic(foo="foo", bar=1),
"my_pydantic": MyPydantic(
foo="foo",
bar=1,
path_keys={pathlib.Path("foo"): MyPydantic(foo="foo", bar=1, path_keys={})},
),
"my_pydantic_class": MyPydantic,
"person": Person(name="foo_person"),
"a_bool": True,
Expand All @@ -842,7 +858,11 @@ class MyNamedTuple(NamedTuple):
"class_with_tee": "tee_a, tee_b",
"my_dataclass": {"foo": "foo", "bar": 1},
"my_enum": "foo",
"my_pydantic": {"foo": "foo", "bar": 1},
"my_pydantic": {
"foo": "foo",
"bar": 1,
"path_keys": {"foo": {"foo": "foo", "bar": 1, "path_keys": {}}},
},
"my_pydantic_class": lambda x: "MyPydantic" in x,
"person": {"name": "foo_person"},
"a_bool": True,
Expand Down