Skip to content

Commit 6fab03a

Browse files
authored
Fix/deep serialize jsons (#707)
* fix: adjust make_json_safe to fit more cases * chore: release 0.0.22
1 parent 289cba6 commit 6fab03a

File tree

2 files changed

+76
-44
lines changed

2 files changed

+76
-44
lines changed

integrations/langgraph/python/ag_ui_langgraph/utils.py

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import json
22
import re
33
from enum import Enum
4+
5+
from pydantic import TypeAdapter
6+
from pydantic_core import PydanticSerializationError
47
from typing import List, Any, Dict, Union
58
from dataclasses import is_dataclass, asdict
69
from datetime import date, datetime
@@ -303,59 +306,87 @@ def json_safe_stringify(o):
303306
return o.isoformat()
304307
return str(o) # last resort
305308

306-
def is_json_primitive(value: Any) -> bool:
307-
return isinstance(value, (str, int, float, bool)) or value is None
308-
309-
def make_json_safe(value: Any) -> Any:
309+
def make_json_safe(value: Any, _seen: set[int] | None = None) -> Any:
310310
"""
311-
Recursively convert a value into a JSON-serializable structure.
312-
313-
- Handles Pydantic models via `model_dump`.
314-
- Handles LangChain messages via `to_dict`.
315-
- Recursively walks dicts, lists, and tuples.
316-
- For arbitrary objects, falls back to `__dict__` if available, else `repr()`.
311+
Convert `value` into something that `json.dumps` can always handle.
312+
313+
Rules (in order):
314+
- primitives → as-is
315+
- Enum → its .value (recursively made safe)
316+
- dict → keys & values made safe
317+
- list/tuple/set/frozenset → list of safe values
318+
- dataclasses → asdict() then recurse
319+
- Pydantic-style models → model_dump()/dict()/to_dict() then recurse
320+
- objects with __dict__ → vars(obj) then recurse
321+
- everything else → repr(obj)
322+
323+
Cycles are detected and replaced with the string "<recursive>".
317324
"""
318-
# Pydantic models
319-
if hasattr(value, "model_dump"):
325+
if _seen is None:
326+
_seen = set()
327+
328+
obj_id = id(value)
329+
if obj_id in _seen:
330+
return "<recursive>"
331+
332+
# --- 1. Primitives -----------------------------------------------------
333+
if isinstance(value, (str, int, float, bool)) or value is None:
334+
return value
335+
336+
# --- 2. Enum → use underlying value -----------------------------------
337+
if isinstance(value, Enum):
338+
return make_json_safe(value.value, _seen)
339+
340+
# --- 3. Dicts ----------------------------------------------------------
341+
if isinstance(value, dict):
342+
_seen.add(obj_id)
343+
return {
344+
make_json_safe(k, _seen): make_json_safe(v, _seen)
345+
for k, v in value.items()
346+
}
347+
348+
# --- 4. Iterable containers -------------------------------------------
349+
if isinstance(value, (list, tuple, set, frozenset)):
350+
_seen.add(obj_id)
351+
return [make_json_safe(v, _seen) for v in value]
352+
353+
# --- 5. Dataclasses ----------------------------------------------------
354+
if is_dataclass(value):
355+
_seen.add(obj_id)
356+
return make_json_safe(asdict(value), _seen)
357+
358+
# --- 6. Pydantic-like models (v2: model_dump) -------------------------
359+
if hasattr(value, "model_dump") and callable(getattr(value, "model_dump")):
360+
_seen.add(obj_id)
320361
try:
321-
return make_json_safe(value.model_dump(by_alias=True, exclude_none=True))
362+
return make_json_safe(value.model_dump(), _seen)
322363
except Exception:
364+
# fall through to other options
323365
pass
324366

325-
# LangChain-style objects
326-
if hasattr(value, "to_dict"):
367+
# --- 7. Pydantic v1-style / other libs with .dict() -------------------
368+
if hasattr(value, "dict") and callable(getattr(value, "dict")):
369+
_seen.add(obj_id)
327370
try:
328-
return make_json_safe(value.to_dict())
371+
return make_json_safe(value.dict(), _seen)
329372
except Exception:
330373
pass
331374

332-
# Dict
333-
if isinstance(value, dict):
334-
return {key: make_json_safe(sub_value) for key, sub_value in value.items()}
335-
336-
# List / tuple
337-
if isinstance(value, (list, tuple)):
338-
return [make_json_safe(sub_value) for sub_value in value]
339-
340-
if isinstance(value, Enum):
341-
enum_value = value.value
342-
if is_json_primitive(enum_value):
343-
return enum_value
344-
return {
345-
"__type__": type(value).__name__,
346-
"name": value.name,
347-
"value": make_json_safe(enum_value),
348-
}
349-
350-
# Already JSON safe
351-
if is_json_primitive(value):
352-
return value
375+
# --- 8. Generic "to_dict" pattern -------------------------------------
376+
if hasattr(value, "to_dict") and callable(getattr(value, "to_dict")):
377+
_seen.add(obj_id)
378+
try:
379+
return make_json_safe(value.to_dict(), _seen)
380+
except Exception:
381+
pass
353382

354-
# Arbitrary object: try __dict__ first, fallback to repr
383+
# --- 9. Generic Python objects with __dict__ --------------------------
355384
if hasattr(value, "__dict__"):
356-
return {
357-
"__type__": type(value).__name__,
358-
**make_json_safe(value.__dict__),
359-
}
385+
_seen.add(obj_id)
386+
try:
387+
return make_json_safe(vars(value), _seen)
388+
except Exception:
389+
pass
360390

361-
return repr(value)
391+
# --- 10. Last resort ---------------------------------------------------
392+
return repr(value)

integrations/langgraph/python/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "ag-ui-langgraph"
3-
version = "0.0.21"
3+
version = "0.0.22"
44
description = "Implementation of the AG-UI protocol for LangGraph."
55
authors = ["Ran Shem Tov <ran@copilotkit.ai>"]
66
readme = "README.md"
@@ -15,6 +15,7 @@ fastapi = { version = "^0.115.12", optional = true }
1515
langchain = ">=0.3.0"
1616
langchain-core = ">=0.3.0"
1717
langgraph = ">=0.3.25,<1.1.0"
18+
pydantic = ">=2.0.0"
1819

1920
[tool.poetry.extras]
2021
fastapi = ["fastapi"]

0 commit comments

Comments
 (0)