@@ -561,6 +561,91 @@ def test_checkpoint_with_messages(redis_url: str) -> None:
561561 assert loaded_messages [1 ].content == "Let me check that for you."
562562
563563
564+ def test_issue_181_msgpack_checkpoint_keeps_messages_json_safe () -> None :
565+ """Ensure msgpack fallback does not leak live message objects into Redis JSON."""
566+ from langchain_core .messages import AIMessage , HumanMessage
567+
568+ from langgraph .checkpoint .redis .base import BaseRedisSaver
569+
570+ class DummySaver (BaseRedisSaver ):
571+ def create_indexes (self ) -> None :
572+ pass
573+
574+ def configure_client (self , ** kwargs ) -> None :
575+ self ._redis = None
576+
577+ saver = DummySaver (redis_client = object ())
578+ messages = [
579+ HumanMessage (content = "What is the weather in SF?" ),
580+ AIMessage (content = "Let me check that for you." ),
581+ ]
582+
583+ checkpoint = create_checkpoint (
584+ checkpoint = empty_checkpoint (),
585+ channels = {"messages" : messages , "binary" : b"abc" },
586+ step = 1 ,
587+ )
588+ checkpoint ["channel_values" ]["messages" ] = messages
589+ checkpoint ["channel_values" ]["binary" ] = b"abc"
590+
591+ dumped = saver ._dump_checkpoint (checkpoint )
592+
593+ # The mixed bytes payload forces msgpack fallback, but the Redis document
594+ # must still be fully JSON-safe.
595+ assert dumped ["type" ] == "msgpack"
596+ assert isinstance (dumped ["channel_values" ]["messages" ][0 ], dict )
597+ assert dumped ["channel_values" ]["messages" ][0 ]["id" ][- 1 ] == "HumanMessage"
598+ assert dumped ["channel_values" ]["binary" ] == {"__bytes__" : "YWJj" }
599+
600+ restored = saver ._load_checkpoint (
601+ dumped ,
602+ saver ._deserialize_channel_values (dumped ["channel_values" ]),
603+ [],
604+ )
605+
606+ restored_messages = restored ["channel_values" ]["messages" ]
607+ assert isinstance (restored_messages [0 ], HumanMessage )
608+ assert isinstance (restored_messages [1 ], AIMessage )
609+ assert restored ["channel_values" ]["binary" ] == b"abc"
610+
611+
612+ def test_msgpack_checkpoint_with_non_string_keys_remains_json_safe () -> None :
613+ """Ensure msgpack checkpoints with non-string keys still normalize for Redis JSON."""
614+ from langchain_core .messages import HumanMessage
615+
616+ from langgraph .checkpoint .redis .base import BaseRedisSaver
617+
618+ class DummySaver (BaseRedisSaver ):
619+ def create_indexes (self ) -> None :
620+ pass
621+
622+ def configure_client (self , ** kwargs ) -> None :
623+ self ._redis = None
624+
625+ saver = DummySaver (redis_client = object ())
626+ messages = [HumanMessage (content = "hello" )]
627+
628+ checkpoint = create_checkpoint (
629+ checkpoint = empty_checkpoint (),
630+ channels = {
631+ "messages" : messages ,
632+ "binary" : b"abc" ,
633+ "mapping" : {1 : "one" , 2 : "two" },
634+ },
635+ step = 1 ,
636+ )
637+ checkpoint ["channel_values" ]["messages" ] = messages
638+ checkpoint ["channel_values" ]["binary" ] = b"abc"
639+ checkpoint ["channel_values" ]["mapping" ] = {1 : "one" , 2 : "two" }
640+
641+ dumped = saver ._dump_checkpoint (checkpoint )
642+
643+ assert dumped ["type" ] == "msgpack"
644+ assert dumped ["channel_values" ]["mapping" ] == {"1" : "one" , "2" : "two" }
645+ assert isinstance (dumped ["channel_values" ]["messages" ][0 ], dict )
646+ assert dumped ["channel_values" ]["binary" ] == {"__bytes__" : "YWJj" }
647+
648+
564649def test_subgraph_state_history_pending_sends (redis_url : str ) -> None :
565650 """Test that get_state_history with subgraphs properly handles pending_sends.
566651
0 commit comments