Skip to content

fix(serializer): recurse converted values through default() to prevent invalid JSON - #1877

Open
betacatsling wants to merge 1 commit into
langfuse:mainfrom
betacatsling:fix/serializer-recurse-converted-values
Open

betacatsling wants to merge 1 commit into
langfuse:mainfrom
betacatsling:fix/serializer-recurse-converted-values

Conversation

@betacatsling

@betacatsling betacatsling commented Sep 14, 2026 •

Copy link
Copy Markdown

What does this PR do?

EventSerializer sanitizes NaN/Infinity/-Infinity floats and JS-unsafe
integers only when a value reaches _default_inner directly. Several
conversion branches returned containers or scalars without routing them back
through self.default, so non-finite floats nested inside them were emitted as
bare NaN/Infinity tokens — invalid JSON that strict parsers reject.

This routes the converted values back through self.default for:

  • tuple / set / frozenset (per element, instead of list(obj))
  • np.ndarray (after .tolist())
  • np.generic scalars (after .item(), e.g. np.float32("nan"))
  • dataclasses (the dict produced by asdict())
  • enum.Enum values (e.g. an enum wrapping float("nan"))
  • LangChain Serializable.to_json() output

Same class of bug as #1811 (which covered Pydantic model_dump() output);
this covers the remaining conversion branches.

Fixes #1876

Type of change

  • Bug fix

Verification

New tests in tests/unit/test_serializer.py assert strict-JSON-parseable
output (rejecting bare NaN/Infinity constants) and the expected string
sentinels for non-finite floats and JS-unsafe integers nested in tuples, sets,
frozensets, dataclasses, enums, numpy arrays, and numpy scalars.

Commands and results:

  • uv run --frozen pytest tests/unit/test_serializer.py -q
    • before: 1 failed, 30 passed — test_non_finite_floats_in_tuple_set_frozenset
      fails with a bare NaN token
    • after: 35 passed
  • uv run --frozen ruff check langfuse/_utils/serializer.py tests/unit/test_serializer.py: all checks passed
  • uv run --frozen ruff format --check ...: already formatted
  • uv run --frozen mypy langfuse --no-error-summary: clean

Checklist

  • I self-reviewed the diff
  • I added or updated tests for behavior changes.
  • I did not hand-edit generated files; if generated files changed, I used the upstream regeneration path.
  • I did not commit secrets or credentials.

RetriggerConfidence Score: 4/5

The core fix appears safe to merge, with non-blocking edge cases around extended NumPy scalars, dataclass mapping keys, and CI coverage.

Summary

  • Extends normalization to NumPy values, dataclasses, enums, LangChain serializables, tuples, sets, and frozensets.
  • Adds strict-JSON assertions for the newly covered value types.
  • Leaves edge cases around self-returning NumPy scalars and converted mapping keys, while NumPy coverage is not guaranteed to run in locked CI.

Reviews (1) · Last reviewed commit: "fix(serializer): recurse converted value..."

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

# integers are normalized
if np is not None and isinstance(obj, np.generic):
return obj.item()
return self.default(obj.item())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 NumPy Scalars Recurse Indefinitely

Extended-precision scalars such as np.longdouble and np.clongdouble may return themselves from .item(). Passing that result back to self.default() repeatedly re-enters this branch without reaching the later depth guard, eventually hitting Python's recursion limit instead of producing a useful serialized value.

Prompt To Fix With AI
This is a comment left during a code review.
Path: langfuse/_utils/serializer.py
Line: 76

Comment:
**NumPy Scalars Recurse Indefinitely**

Extended-precision scalars such as `np.longdouble` and `np.clongdouble` may return themselves from `.item()`. Passing that result back to `self.default()` repeatedly re-enters this branch without reaching the later depth guard, eventually hitting Python's recursion limit instead of producing a useful serialized value.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

if is_dataclass(obj):
return asdict(obj) # type: ignore
# Recurse the dict produced by asdict() back through default()
# so nested non-finite floats and unsafe integers are normalized

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Dataclass Keys Defeat Skipkeys

A dataclass field containing a mapping with tuple keys is now recursively processed before the standard encoder can apply skipkeys=True. Each tuple key becomes an unhashable list while rebuilding the dictionary, so the entire mapping is replaced by the serializer's error marker instead of omitting the unsupported key.

Prompt To Fix With AI
This is a comment left during a code review.
Path: langfuse/_utils/serializer.py
Line: 135

Comment:
**Dataclass Keys Defeat Skipkeys**

A dataclass field containing a mapping with tuple keys is now recursively processed before the standard encoder can apply `skipkeys=True`. Each tuple key becomes an unhashable list while rebuilding the dictionary, so the entire mapping is replaced by the serializer's error marker instead of omitting the unsupported key.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EventSerializer emits invalid JSON for non-finite floats nested inside tuples, sets, numpy arrays, dataclasses, enums, and LangChain Serializable output

2 participants