11import json
22import re
33from enum import Enum
4+
5+ from pydantic import TypeAdapter
6+ from pydantic_core import PydanticSerializationError
47from typing import List , Any , Dict , Union
58from dataclasses import is_dataclass , asdict
69from 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 )
0 commit comments