From 79ed64bab934ff6e97bfbe9d994b46f648a357aa Mon Sep 17 00:00:00 2001 From: Harbor404 <2657212322@qq.com> Date: Fri, 2 Oct 2026 13:05:22 +0800 Subject: [PATCH] fix: validate top-level JSON arrays in JsonSchemaValidator Validate the complete payload once for regular schemas and only validate individual tool calls when the payload is an OpenAI function-calling list. --- haystack/components/validators/json_schema.py | 36 +++++++++++++++---- ...tor-array-validation-05344b7b7b18680b.yaml | 7 ++++ .../components/validators/test_json_schema.py | 18 ++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/fix-json-schema-validator-array-validation-05344b7b7b18680b.yaml diff --git a/haystack/components/validators/json_schema.py b/haystack/components/validators/json_schema.py index 2f168bf9365..f9e5e062b8a 100644 --- a/haystack/components/validators/json_schema.py +++ b/haystack/components/validators/json_schema.py @@ -161,12 +161,18 @@ def run( else: validation_schema = json_schema try: - last_message_json = [last_message_json] if not isinstance(last_message_json, list) else last_message_json - for content in last_message_json: - if using_openai_schema: - validate(instance=content["function"]["arguments"], schema=validation_schema) - else: - validate(instance=content, schema=validation_schema) + is_function_calling_payload = using_openai_schema or self._is_function_calling_message_list( + last_message_json + ) + if is_function_calling_payload: + last_message_json = last_message_json if isinstance(last_message_json, list) else [last_message_json] + for content in last_message_json: + if using_openai_schema: + validate(instance=content["function"]["arguments"], schema=validation_schema) + else: + validate(instance=content, schema=validation_schema) + else: + validate(instance=last_message_json, schema=validation_schema) return {"validated": [last_message]} except ValidationError as e: @@ -218,6 +224,24 @@ def _is_openai_function_calling_schema(self, json_schema: dict[str, Any]) -> boo """ return all(key in json_schema for key in ["name", "description", "parameters"]) + def _is_function_calling_message_list(self, data: Any) -> bool: + """ + Checks if the provided data is a non-empty list of OpenAI function calling messages. + + :param data: The data to check. + :return: `True` if the data is a function calling message list; otherwise, `False`. + """ + if not isinstance(data, list) or not data: + return False + + for message in data: + if not isinstance(message, dict): + return False + function = message.get("function") + if not isinstance(function, dict) or "arguments" not in function: + return False + return True + def _recursive_json_to_object(self, data: Any) -> Any: """ Convert any string values that are valid JSON objects into dictionary objects. diff --git a/releasenotes/notes/fix-json-schema-validator-array-validation-05344b7b7b18680b.yaml b/releasenotes/notes/fix-json-schema-validator-array-validation-05344b7b7b18680b.yaml new file mode 100644 index 00000000000..82ebcd11ae4 --- /dev/null +++ b/releasenotes/notes/fix-json-schema-validator-array-validation-05344b7b7b18680b.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed ``JsonSchemaValidator`` rejecting valid top-level JSON arrays when the provided schema describes an array. + Regular schemas now validate the complete payload once, while OpenAI function-calling payloads continue to + validate each tool call individually. Array constraints such as ``minItems`` are now also enforced for empty + arrays. diff --git a/test/components/validators/test_json_schema.py b/test/components/validators/test_json_schema.py index 49f8eea8397..2e443ee1bcc 100644 --- a/test/components/validators/test_json_schema.py +++ b/test/components/validators/test_json_schema.py @@ -141,6 +141,24 @@ def test_validation_error_for_top_level_json_scalar(self, message_text): assert "validation_error" in result assert len(result["validation_error"]) == 1 + def test_validates_top_level_json_array(self): + validator = JsonSchemaValidator(json_schema={"type": "array", "items": {"type": "integer"}}) + message = ChatMessage.from_assistant("[1, 2]") + + result = validator.run([message]) + + assert result == {"validated": [message]} + + def test_rejects_empty_top_level_json_array(self): + validator = JsonSchemaValidator(json_schema={"type": "array", "items": {"type": "integer"}, "minItems": 1}) + message = ChatMessage.from_assistant("[]") + + result = validator.run([message]) + + assert "validation_error" in result + assert len(result["validation_error"]) == 1 + assert "should be non-empty" in result["validation_error"][0].text + # Validates multiple messages against a provided JSON schema successfully. def test_validates_multiple_messages_against_json_schema(self, json_schema_github_compare, genuine_fc_message): validator = JsonSchemaValidator()