@@ -216,6 +216,113 @@ def convert_timestamp_to_datetime(obj, model_fields):
216216 return obj
217217
218218
219+ def convert_bytes_to_base64 (obj ):
220+ """Convert bytes objects to base64-encoded strings for storage.
221+
222+ This is necessary because Redis JSON and the jsonable_encoder cannot
223+ handle arbitrary binary data. Base64 encoding ensures all byte values
224+ (0-255) can be safely stored and retrieved.
225+ """
226+ import base64
227+
228+ if isinstance (obj , dict ):
229+ return {key : convert_bytes_to_base64 (value ) for key , value in obj .items ()}
230+ elif isinstance (obj , list ):
231+ return [convert_bytes_to_base64 (item ) for item in obj ]
232+ elif isinstance (obj , bytes ):
233+ return base64 .b64encode (obj ).decode ("ascii" )
234+ else :
235+ return obj
236+
237+
238+ def convert_base64_to_bytes (obj , model_fields ):
239+ """Convert base64-encoded strings back to bytes based on model field types."""
240+ import base64
241+
242+ if isinstance (obj , dict ):
243+ result = {}
244+ for key , value in obj .items ():
245+ if key in model_fields :
246+ field_info = model_fields [key ]
247+ field_type = (
248+ field_info .annotation if hasattr (field_info , "annotation" ) else None
249+ )
250+
251+ # Handle Optional types - extract the inner type
252+ if hasattr (field_type , "__origin__" ) and field_type .__origin__ is Union :
253+ # For Optional[T] which is Union[T, None], get the non-None type
254+ args = getattr (field_type , "__args__" , ())
255+ non_none_types = [
256+ arg for arg in args if arg is not type (None ) # noqa: E721
257+ ]
258+ if len (non_none_types ) == 1 :
259+ field_type = non_none_types [0 ]
260+
261+ # Handle bytes fields
262+ if field_type is bytes and isinstance (value , str ):
263+ try :
264+ result [key ] = base64 .b64decode (value )
265+ except (ValueError , TypeError ):
266+ # If it's not valid base64, keep original value
267+ result [key ] = value
268+ # Handle nested models - check if it's a model with fields
269+ elif isinstance (value , dict ):
270+ try :
271+ if (
272+ isinstance (field_type , type )
273+ and hasattr (field_type , "model_fields" )
274+ and field_type .model_fields
275+ ):
276+ result [key ] = convert_base64_to_bytes (
277+ value , field_type .model_fields
278+ )
279+ else :
280+ result [key ] = convert_base64_to_bytes (value , {})
281+ except (TypeError , AttributeError ):
282+ result [key ] = convert_base64_to_bytes (value , {})
283+ # Handle lists that might contain nested models
284+ elif isinstance (value , list ):
285+ # Try to extract the inner type from List[SomeModel]
286+ inner_type = None
287+ if (
288+ hasattr (field_type , "__origin__" )
289+ and field_type .__origin__ in (list , List )
290+ and hasattr (field_type , "__args__" )
291+ and field_type .__args__
292+ ):
293+ inner_type = field_type .__args__ [0 ]
294+
295+ if inner_type is not None :
296+ try :
297+ if (
298+ isinstance (inner_type , type )
299+ and hasattr (inner_type , "model_fields" )
300+ and inner_type .model_fields
301+ ):
302+ result [key ] = [
303+ convert_base64_to_bytes (item , inner_type .model_fields )
304+ if isinstance (item , dict )
305+ else item
306+ for item in value
307+ ]
308+ else :
309+ result [key ] = convert_base64_to_bytes (value , {})
310+ except (TypeError , AttributeError ):
311+ result [key ] = convert_base64_to_bytes (value , {})
312+ else :
313+ result [key ] = convert_base64_to_bytes (value , {})
314+ else :
315+ result [key ] = convert_base64_to_bytes (value , {})
316+ else :
317+ # For keys not in model_fields, still recurse but with empty field info
318+ result [key ] = convert_base64_to_bytes (value , {})
319+ return result
320+ elif isinstance (obj , list ):
321+ return [convert_base64_to_bytes (item , model_fields ) for item in obj ]
322+ else :
323+ return obj
324+
325+
219326class PartialModel :
220327 """A partial model instance that only contains certain fields.
221328
@@ -2558,10 +2665,14 @@ def to_string(s):
25582665 json_fields = convert_timestamp_to_datetime (
25592666 json_fields , cls .model_fields
25602667 )
2668+ # Convert base64 strings back to bytes for bytes fields
2669+ json_fields = convert_base64_to_bytes (json_fields , cls .model_fields )
25612670 doc = cls (** json_fields )
25622671 else :
25632672 # Convert timestamps back to datetime objects
25642673 fields = convert_timestamp_to_datetime (fields , cls .model_fields )
2674+ # Convert base64 strings back to bytes for bytes fields
2675+ fields = convert_base64_to_bytes (fields , cls .model_fields )
25652676 doc = cls (** fields )
25662677
25672678 docs .append (doc )
@@ -2752,9 +2863,10 @@ async def save(
27522863 self .check ()
27532864 db = self ._get_db (pipeline )
27542865
2755- # Get model data and convert datetime objects first
2866+ # Get model data and apply conversions in the correct order
27562867 document = self .model_dump ()
27572868 document = convert_datetime_to_timestamp (document )
2869+ document = convert_bytes_to_base64 (document )
27582870
27592871 # Then apply jsonable encoding for other types
27602872 document = jsonable_encoder (document )
@@ -2854,6 +2966,8 @@ async def get(cls: Type["Model"], pk: Any) -> "Model":
28542966 try :
28552967 # Convert timestamps back to datetime objects before validation
28562968 document = convert_timestamp_to_datetime (document , cls .model_fields )
2969+ # Convert base64 strings back to bytes for bytes fields
2970+ document = convert_base64_to_bytes (document , cls .model_fields )
28572971 result = cls .model_validate (document )
28582972 except TypeError as e :
28592973 log .warning (
@@ -2865,6 +2979,8 @@ async def get(cls: Type["Model"], pk: Any) -> "Model":
28652979 document = decode_redis_value (document , cls .Meta .encoding )
28662980 # Convert timestamps back to datetime objects after decoding
28672981 document = convert_timestamp_to_datetime (document , cls .model_fields )
2982+ # Convert base64 strings back to bytes for bytes fields
2983+ document = convert_base64_to_bytes (document , cls .model_fields )
28682984 result = cls .model_validate (document )
28692985 return result
28702986
@@ -3126,6 +3242,8 @@ async def save(
31263242 data = self .model_dump ()
31273243 # Convert datetime objects to timestamps for proper indexing
31283244 data = convert_datetime_to_timestamp (data )
3245+ # Convert bytes to base64 strings for safe JSON storage
3246+ data = convert_bytes_to_base64 (data )
31293247 # Apply JSON encoding for complex types (Enums, UUIDs, Sets, etc.)
31303248 data = jsonable_encoder (data )
31313249
@@ -3199,6 +3317,8 @@ async def get(cls: Type["Model"], pk: Any) -> "Model":
31993317 raise NotFoundError
32003318 # Convert timestamps back to datetime objects before validation
32013319 document_data = convert_timestamp_to_datetime (document_data , cls .model_fields )
3320+ # Convert base64 strings back to bytes for bytes fields
3321+ document_data = convert_base64_to_bytes (document_data , cls .model_fields )
32023322 return cls .model_validate (document_data )
32033323
32043324 @classmethod
0 commit comments