From 2d8100c4faef2e4a0ec7ab74536fe7a9d9ae551e Mon Sep 17 00:00:00 2001 From: Mohammad Mohtashim <45242107+keenborder786@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:15:21 +0500 Subject: [PATCH] fix(langchain-classic): fix `Chain.save()` regression from dict-to-`model_dump` migration (#35667) Fixes #35665 --- Fixes a regression introduced in #33035 where `Chain.save()` stopped working for chain types that support saving, including `LLMChain`. `Chain.save()` now calls `self.model_dump()`, but the `_type` injection still lived in the deprecated `dict()` override. As a result, serialized chains no longer included `_type`, and `save()` always raised the "does not support saving" error. This moves the override to `model_dump()` so saved chain output includes `_type` again. Pydantic v2's `dict()` method delegates to `model_dump()`, so existing `dict()` callers continue to get the same serialized shape. --- .../langchain_classic/chains/base.py | 6 +- .../tests/unit_tests/chains/test_base.py | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/libs/langchain/langchain_classic/chains/base.py b/libs/langchain/langchain_classic/chains/base.py index 1f9c4add76f..65051721cf3 100644 --- a/libs/langchain/langchain_classic/chains/base.py +++ b/libs/langchain/langchain_classic/chains/base.py @@ -732,15 +732,15 @@ class Chain(RunnableSerializable[dict[str, Any], dict[str, Any]], ABC): ) raise ValueError(msg) - def dict(self, **kwargs: Any) -> dict: + def model_dump(self, **kwargs: Any) -> dict: """Dictionary representation of chain. Expects `Chain._chain_type` property to be implemented and for memory to be null. Args: - **kwargs: Keyword arguments passed to default `pydantic.BaseModel.dict` - method. + **kwargs: Keyword arguments passed to default + `pydantic.BaseModel.model_dump` method. Returns: A dictionary representation of the chain. diff --git a/libs/langchain/tests/unit_tests/chains/test_base.py b/libs/langchain/tests/unit_tests/chains/test_base.py index a607eada5b9..bd5d83625e4 100644 --- a/libs/langchain/tests/unit_tests/chains/test_base.py +++ b/libs/langchain/tests/unit_tests/chains/test_base.py @@ -2,6 +2,7 @@ import re import uuid +from pathlib import Path from typing import Any import pytest @@ -66,6 +67,14 @@ class FakeChain(Chain): return {"baz": "bar"} +class FakeSavableChain(FakeChain): + """Fake chain that supports saving via _chain_type.""" + + @property + def _chain_type(self) -> str: + return "fake_savable" + + def test_bad_inputs() -> None: """Test errors are raised if input keys are not found.""" chain = FakeChain() @@ -243,3 +252,51 @@ def test_run_with_callback_and_output_error() -> None: assert handler.starts == 1 assert handler.ends == 0 assert handler.errors == 1 + + +def test_model_dump_includes_type() -> None: + """Test that model_dump includes _type when _chain_type is implemented.""" + chain = FakeSavableChain() + dumped = chain.model_dump() + assert "_type" in dumped + assert dumped["_type"] == "fake_savable" + + +def test_model_dump_excludes_type_when_not_implemented() -> None: + """Test that model_dump omits _type when _chain_type raises.""" + chain = FakeChain() + dumped = chain.model_dump() + assert "_type" not in dumped + + +def test_save_yaml(tmp_path: Path) -> None: + """Test that save() works for a chain that implements _chain_type.""" + chain = FakeSavableChain() + file_path = tmp_path / "chain.yaml" + chain.save(str(file_path)) + assert file_path.exists() + import yaml + + with file_path.open() as f: + data = yaml.safe_load(f) + assert data["_type"] == "fake_savable" + + +def test_save_json(tmp_path: Path) -> None: + """Test that save() works with JSON format.""" + chain = FakeSavableChain() + file_path = tmp_path / "chain.json" + chain.save(str(file_path)) + assert file_path.exists() + import json + + with file_path.open() as f: + data = json.load(f) + assert data["_type"] == "fake_savable" + + +def test_save_raises_when_chain_type_not_implemented(tmp_path: Path) -> None: + """Test that save() raises NotImplementedError for unsavable chains.""" + chain = FakeChain() + with pytest.raises(NotImplementedError, match="does not support saving"): + chain.save(str(tmp_path / "chain.yaml"))