88import pydantic
99
1010from haystack import logging
11- from haystack .core .errors import DeserializationError
11+ from haystack .core .errors import DeserializationError , SerializationError
1212from haystack .core .serialization import generate_qualified_class_name , import_class_by_name
1313from haystack .utils import deserialize_callable , serialize_callable
1414
@@ -60,7 +60,7 @@ def _serialize_value_with_schema(payload: Any) -> dict[str, Any]: # noqa: PLR09
6060 return {"serialization_schema" : {"type" : "object" , "properties" : schema }, "serialized_data" : data }
6161
6262 # Handle array case - iterate through elements
63- if isinstance (payload , (list , tuple , set )):
63+ if isinstance (payload , (list , tuple , set , frozenset )):
6464 # Serialize each item in the array
6565 serialized_list = []
6666 for item in payload :
@@ -76,9 +76,12 @@ def _serialize_value_with_schema(payload: Any) -> dict[str, Any]: # noqa: PLR09
7676 else :
7777 base_schema = {"type" : "array" , "items" : {}}
7878
79- # Add JSON Schema properties to infer sets and tuples
80- if isinstance (payload , set ):
79+ # Add JSON Schema properties to infer sets, frozensets and tuples
80+ if isinstance (payload , ( set , frozenset ) ):
8181 base_schema ["uniqueItems" ] = True
82+ # `frozen` distinguishes frozenset from set on deserialization
83+ if isinstance (payload , frozenset ):
84+ base_schema ["frozen" ] = True
8285 elif isinstance (payload , tuple ):
8386 base_schema ["minItems" ] = len (payload )
8487 base_schema ["maxItems" ] = len (payload )
@@ -101,19 +104,71 @@ def _serialize_value_with_schema(payload: Any) -> dict[str, Any]: # noqa: PLR09
101104 type_name = generate_qualified_class_name (type (payload ))
102105 return {"serialization_schema" : {"type" : type_name }, "serialized_data" : payload .name }
103106
104- # Handle arbitrary objects with __dict__
105- if hasattr (payload , "__dict__" ):
106- type_name = generate_qualified_class_name (type (payload ))
107- schema = {"type" : type_name }
108- serialized_data = {}
109- for key , value in vars (payload ).items ():
110- serialized_value = _serialize_value_with_schema (value )
111- serialized_data [key ] = serialized_value ["serialized_data" ]
112- return {"serialization_schema" : schema , "serialized_data" : serialized_data }
113-
114107 # Handle primitives
115- schema = {"type" : _primitive_schema_type (payload )}
116- return {"serialization_schema" : schema , "serialized_data" : payload }
108+ if payload is None or isinstance (payload , (bool , int , float , str )):
109+ return {"serialization_schema" : {"type" : _primitive_schema_type (payload )}, "serialized_data" : payload }
110+
111+ # Unsupported type: raise so callers can omit the field (see `_serialize_with_field_fallback`)
112+ # instead of embedding a live, non-portable object in the output.
113+ raise SerializationError (
114+ f"Cannot serialize value of type '{ type (payload ).__name__ } '. Supported values are primitives "
115+ f"(str, int, float, bool, None), lists/tuples/sets/frozensets/dicts of supported values, Enums, "
116+ f"callables, pydantic models, and objects exposing a 'to_dict' method."
117+ )
118+
119+
120+ def _serialize_with_field_fallback (payload : Any , * , description : str ) -> dict [str , Any ]:
121+ """
122+ Serialize a payload and, on failure, retry field-by-field to preserve serializable fields.
123+
124+ If the whole payload serializes, the result is returned as-is. Otherwise, and if the payload is a
125+ mapping, each top-level field is serialized individually and only the failing fields are omitted.
126+ When the payload is not a mapping, or when every field fails to serialize, the helper returns a
127+ structurally valid empty-object payload so that the downstream `_deserialize_value_with_schema`
128+ can still load it back instead of raising `DeserializationError` on a bare `{}`.
129+
130+ :param payload: The value to serialize.
131+ :param description: Short human-readable label used in warning messages, for example
132+ `"the agent's State data"` or `"the inputs of the current pipeline state"`.
133+ :returns: A dict of the form `{"serialization_schema": ..., "serialized_data": ...}`.
134+ """
135+ # Local import to avoid a circular import at module load time.
136+ from haystack .core .pipeline .utils import _deepcopy_with_exceptions
137+
138+ try :
139+ return _serialize_value_with_schema (_deepcopy_with_exceptions (payload ))
140+ except Exception as error :
141+ logger .warning (
142+ "Failed to serialize {description}. "
143+ "Haystack will omit only the non-serializable fields when possible. Error: {e}" ,
144+ description = description ,
145+ e = error ,
146+ )
147+
148+ serialized_properties : dict [str , Any ] = {}
149+ serialized_data : dict [str , Any ] = {}
150+
151+ if isinstance (payload , dict ):
152+ for field_name , value in payload .items ():
153+ try :
154+ serialized_value = _serialize_value_with_schema (_deepcopy_with_exceptions (value ))
155+ except Exception as field_error :
156+ logger .warning (
157+ "Failed to serialize the '{field_name}' field of {description}. "
158+ "The field will be omitted from the snapshot. Error: {e}" ,
159+ field_name = field_name ,
160+ description = description ,
161+ e = field_error ,
162+ )
163+ continue
164+
165+ serialized_properties [field_name ] = serialized_value ["serialization_schema" ]
166+ serialized_data [field_name ] = serialized_value ["serialized_data" ]
167+
168+ return {
169+ "serialization_schema" : {"type" : "object" , "properties" : serialized_properties },
170+ "serialized_data" : serialized_data ,
171+ }
117172
118173
119174def _primitive_schema_type (value : Any ) -> str :
@@ -183,10 +238,10 @@ def _deserialize_value_with_schema(serialized: dict[str, Any]) -> Any:
183238 _deserialize_value_with_schema ({"serialization_schema" : schema ["items" ], "serialized_data" : item })
184239 for item in data
185240 ]
186- final_array : list | set | tuple
187- # Is a set if uniqueItems is True
241+ final_array : list | set | frozenset | tuple
242+ # Is a set or frozenset if uniqueItems is True
188243 if schema .get ("uniqueItems" ) is True :
189- final_array = set (deserialized_items )
244+ final_array = frozenset ( deserialized_items ) if schema . get ( "frozen" ) is True else set (deserialized_items )
190245 # Is a tuple if minItems and maxItems are set
191246 elif schema .get ("minItems" ) is not None and schema .get ("maxItems" ) is not None :
192247 final_array = tuple (deserialized_items )
0 commit comments