From 267ec9e42dc11f0a98ef9d4f3e204407e1a7da1a Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Sat, 27 Jun 2026 00:12:54 +0300 Subject: [PATCH] fix(react): leave list[str] items uncoerced when a schema is provided coerce_json_strings protects scalar str-typed fields from coercion when a schema is supplied, but the list branch of _coerce_field never extended that protection to list[str] item types: each element was passed to blind coercion, so a string element that merely looks like JSON ('{"a": 1}', '[1, 2]') was silently parsed into a dict/list against the declared str type. Guard the list branch the same way the scalar branch is guarded: when the (optional-unwrapped) item type is str, leave the list elements untouched. Model-typed and unknown item types are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../agent/react/json_utils.py | 10 +++-- tests/agent/react/test_json_utils.py | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/uipath_langchain/agent/react/json_utils.py b/src/uipath_langchain/agent/react/json_utils.py index fe1570771..199d52d6f 100644 --- a/src/uipath_langchain/agent/react/json_utils.py +++ b/src/uipath_langchain/agent/react/json_utils.py @@ -238,10 +238,14 @@ def _coerce_field(key: str, value: Any, schema: type[BaseModel] | None) -> Any: if get_origin(annotation) is list: item_args = get_args(annotation) - item_schema = None - if item_args and _is_pydantic_model(item_args[0]): - item_schema = item_args[0] if isinstance(value, list): + # str-typed items are protected like a scalar str field: a list[str] + # element that merely looks like JSON must stay a string. + if item_args and _unwrap_optional(item_args[0]) is str: + return value + item_schema = ( + item_args[0] if item_args and _is_pydantic_model(item_args[0]) else None + ) return [coerce_json_strings(item, item_schema) for item in value] return coerce_json_strings(value) diff --git a/tests/agent/react/test_json_utils.py b/tests/agent/react/test_json_utils.py index 80d290ede..f8d1c88ac 100644 --- a/tests/agent/react/test_json_utils.py +++ b/tests/agent/react/test_json_utils.py @@ -532,3 +532,40 @@ def test_get_job_attachments_after_second_build(self) -> None: attachments = get_job_attachments(first, self._data()) assert [str(att.id) for att in attachments] == [self._ATTACHMENT_ID] + + +# -- coerce_json_strings: list[str] items are protected by the schema ---------- + + +class TestCoerceListOfStrProtected: + """The elements of a list[str] field are str-typed per the schema and must be + left untouched, exactly as a scalar str field is — even when an element looks + like JSON or a Python repr.""" + + def test_list_str_items_not_coerced(self) -> None: + class Schema(BaseModel): + tags: list[str] + + data = {"tags": ['{"not": "a dict"}', "hello", "[1, 2]"]} + assert coerce_json_strings(data, Schema) == { + "tags": ['{"not": "a dict"}', "hello", "[1, 2]"] + } + + def test_optional_list_str_items_not_coerced(self) -> None: + class Schema(BaseModel): + tags: Optional[list[str]] = None + + data = {"tags": ["{'a': 1}"]} + assert coerce_json_strings(data, Schema) == {"tags": ["{'a': 1}"]} + + def test_list_model_items_still_coerced(self) -> None: + """Regression guard: model-typed list items must still be coerced.""" + + class Item(BaseModel): + data: dict[str, Any] | None = None + + class Schema(BaseModel): + items: list[Item] + + data = {"items": [{"data": '{"size": 1}'}]} + assert coerce_json_strings(data, Schema) == {"items": [{"data": {"size": 1}}]}