From 254e11583c2b63de1c399579bdd6fc20290c76d9 Mon Sep 17 00:00:00 2001 From: otiscuilei Date: Thu, 9 Jul 2026 09:39:26 +0800 Subject: [PATCH 1/4] fix: preserve metadata key named "meta" in Document.from_dict() A metadata key literally named "meta" produced by the default to_dict(flatten=True) was popped as the meta constructor parameter and splatted with {**meta}, raising TypeError when its value was not a mapping. A non-mapping "meta" value can only be a flattened metadata key, so treat it as one and preserve it through the round-trip. --- haystack/dataclasses/document.py | 7 +++++++ ...ument-meta-key-collision-1330a997893d9f34.yaml | 8 ++++++++ test/dataclasses/test_document.py | 15 +++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 releasenotes/notes/document-meta-key-collision-1330a997893d9f34.yaml diff --git a/haystack/dataclasses/document.py b/haystack/dataclasses/document.py index 7792eaee610..ef0363a4888 100644 --- a/haystack/dataclasses/document.py +++ b/haystack/dataclasses/document.py @@ -165,6 +165,13 @@ def from_dict(cls, data: dict[str, Any]) -> "Document": # a document field is a metadata key. We treat legacy fields as document fields # for backward compatibility. flatten_meta = {} + # A non-mapping value under the "meta" key can't be the `meta` parameter (which must be a + # dictionary). It can only be a flattened metadata key literally named "meta", produced by + # to_dict(flatten=True) for a document whose metadata contains a "meta" key. Treat it as such + # so the value is preserved instead of being splatted into a `dict`, which would raise. + if not isinstance(meta, dict): + flatten_meta["meta"] = meta + meta = {} document_fields = LEGACY_FIELDS + [f.name for f in fields(cls)] for key in list(data.keys()): if key not in document_fields: diff --git a/releasenotes/notes/document-meta-key-collision-1330a997893d9f34.yaml b/releasenotes/notes/document-meta-key-collision-1330a997893d9f34.yaml new file mode 100644 index 00000000000..b6fe06b409c --- /dev/null +++ b/releasenotes/notes/document-meta-key-collision-1330a997893d9f34.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed ``Document.from_dict()`` raising ``TypeError: 'str' object is not a mapping`` when + deserializing a document whose metadata contained a non-mapping key named ``meta`` + (for example ``Document(content="hi", meta={"meta": "value"})``). Such a document is now + correctly reconstructed from the default flattened ``to_dict()`` output, so the + ``to_dict()``/``from_dict()`` round-trip no longer crashes. diff --git a/test/dataclasses/test_document.py b/test/dataclasses/test_document.py index 18e6566328c..018a92ac324 100644 --- a/test/dataclasses/test_document.py +++ b/test/dataclasses/test_document.py @@ -345,6 +345,21 @@ def test_from_dict_with_flat_meta(): ) +def test_to_dict_from_dict_roundtrip_with_meta_key_named_meta(): + """ + Regression test: a metadata key literally named "meta" must survive the default + to_dict(flatten=True)/from_dict() round-trip. + + Previously from_dict() popped the flattened "meta" key as the `meta` constructor + parameter and splatted its (non-mapping) value with `{**meta}`, raising + TypeError: 'str' object is not a mapping. + """ + doc = Document(content="hi", meta={"meta": "value"}) + + assert Document.from_dict(doc.to_dict()) == doc + assert Document.from_dict(doc.to_dict()).meta == {"meta": "value"} + + def test_from_dict_with_flat_and_non_flat_meta(): with pytest.raises(ValueError, match="Pass either the 'meta' parameter or flattened metadata keys"): Document.from_dict( From 1126ea370fe3fee2add9e2a6e7842bbb1b2e53f5 Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee <10526848+sjrl@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:16:47 +0200 Subject: [PATCH 2/4] Apply suggestion from @sjrl --- haystack/dataclasses/document.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/haystack/dataclasses/document.py b/haystack/dataclasses/document.py index ef0363a4888..2658423d358 100644 --- a/haystack/dataclasses/document.py +++ b/haystack/dataclasses/document.py @@ -167,8 +167,7 @@ def from_dict(cls, data: dict[str, Any]) -> "Document": flatten_meta = {} # A non-mapping value under the "meta" key can't be the `meta` parameter (which must be a # dictionary). It can only be a flattened metadata key literally named "meta", produced by - # to_dict(flatten=True) for a document whose metadata contains a "meta" key. Treat it as such - # so the value is preserved instead of being splatted into a `dict`, which would raise. + # to_dict(flatten=True) for a document whose metadata contains a "meta" key. if not isinstance(meta, dict): flatten_meta["meta"] = meta meta = {} From 84b7b2bfbb67750faf7ff553df0e452a2f067ff2 Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee <10526848+sjrl@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:17:59 +0200 Subject: [PATCH 3/4] Apply suggestion from @sjrl --- test/dataclasses/test_document.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/dataclasses/test_document.py b/test/dataclasses/test_document.py index 018a92ac324..d98f09de5a1 100644 --- a/test/dataclasses/test_document.py +++ b/test/dataclasses/test_document.py @@ -349,10 +349,6 @@ def test_to_dict_from_dict_roundtrip_with_meta_key_named_meta(): """ Regression test: a metadata key literally named "meta" must survive the default to_dict(flatten=True)/from_dict() round-trip. - - Previously from_dict() popped the flattened "meta" key as the `meta` constructor - parameter and splatted its (non-mapping) value with `{**meta}`, raising - TypeError: 'str' object is not a mapping. """ doc = Document(content="hi", meta={"meta": "value"}) From c3483db870c0ec093cb482562d3bc2cb3e34e988 Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee <10526848+sjrl@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:18:08 +0200 Subject: [PATCH 4/4] Apply suggestion from @sjrl --- test/dataclasses/test_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dataclasses/test_document.py b/test/dataclasses/test_document.py index d98f09de5a1..994ce3c6167 100644 --- a/test/dataclasses/test_document.py +++ b/test/dataclasses/test_document.py @@ -347,7 +347,7 @@ def test_from_dict_with_flat_meta(): def test_to_dict_from_dict_roundtrip_with_meta_key_named_meta(): """ - Regression test: a metadata key literally named "meta" must survive the default + A metadata key literally named "meta" must survive the default to_dict(flatten=True)/from_dict() round-trip. """ doc = Document(content="hi", meta={"meta": "value"})