Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions haystack/components/validators/json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions test/components/validators/test_json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down