Skip to content
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
5 changes: 4 additions & 1 deletion langfuse/_utils/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ def _default_inner(self, obj: Any) -> Any:
if isinstance(raw := getattr(obj, "raw", None), BaseModel):
raw.model_rebuild()

return obj.model_dump()
# model_dump() returns plain dicts/lists/scalars, so route it back
# through default() to keep nested non-finite floats (NaN/Inf) and
# other non-JSON-safe values sanitized like the dict branch does.
return self.default(obj.model_dump())

# if langchain is not available, the Serializable type is NoneType
if Serializable is not type(None) and isinstance(obj, Serializable): # type: ignore
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ def test_pydantic_model():
assert json.loads(serializer.encode(model)) == {"field": "test"}


def test_pydantic_model_with_non_finite_float_serializes_to_valid_json():
# model_dump() returns a plain dict; the previous BaseModel branch returned
# it directly, so nested NaN/Inf floats bypassed the sanitizer and produced
# bare NaN/Infinity tokens that strict JSON parsers reject. See #16048.
class Scores(BaseModel):
confidence: float

serializer = EventSerializer()
encoded = serializer.encode(Scores(confidence=float("nan")))

# Strict parse must succeed and the NaN token must be a quoted string.
assert json.loads(
encoded, parse_constant=lambda t: (_ for _ in ()).throw(ValueError(t))
) == {"confidence": "NaN"}
assert (
serializer.encode(Scores(confidence=float("inf")))
== '{"confidence": "Infinity"}'
)


def test_langfuse_media_reference_serializes_to_reference_string():
# Resolved references must round-trip back to their original reference string
# rather than falling through to asdict() and emitting an opaque dict.
Expand Down