deepset-ai/haystack · error · ValueError

Input must be a dictionary or a list of dictionaries.

Error message

Input must be a dictionary or a list of dictionaries.

What it means

JsonSchemaValidator's internal _recursive_json_to_object recursively converts JSON strings embedded in dict values into objects. It only accepts a dict or a list at the top level (dicts and lists are recursed; scalars inside dicts are preserved). Passing any other top-level value (str, int, None, etc.) hits the terminal raise ValueError.

Source

Thrown at haystack/components/validators/json_schema.py:252

            new_dict = {}
            for key, value in data.items():
                if isinstance(value, str):
                    try:
                        json_value = json.loads(value)
                        if isinstance(json_value, (dict, list)):
                            new_dict[key] = self._recursive_json_to_object(json_value)
                        else:
                            new_dict[key] = value  # Preserve the original string value
                    except json.JSONDecodeError:
                        new_dict[key] = value
                elif isinstance(value, dict):
                    new_dict[key] = self._recursive_json_to_object(value)
                else:
                    new_dict[key] = value
            return new_dict

        # If it's neither a list nor a dictionary, return the value directly
        raise ValueError("Input must be a dictionary or a list of dictionaries.")

View on GitHub (pinned to e318778c9b)

Solutions

  1. Parse the input before validation: if isinstance(data, str): data = json.loads(data) before calling JsonSchemaValidator.run.
  2. Ensure the LLM output is converted to dict/list (e.g. via an output adapter or JSONOutputParser) before the validator.
  3. If data may be None (e.g. failed generation), check and provide a fallback dict before run().
  4. Check that double-encoded JSON isn't the issue: json.loads may need to be applied twice for nested stringified JSON.

Example fix

// before
result = validator.run(json=llm_output_text)
// after
if isinstance(llm_output_text, str):
    llm_output_text = json.loads(llm_output_text)
result = validator.run(json=llm_output_text)
Defensive patterns

Strategy: validation

Validate before calling

def is_valid_validator_input(data) -> bool:
    return isinstance(data, (dict, list)) and not (isinstance(data, list) and any(not isinstance(i, dict) for i in data))
# guard: if isinstance(data, str): data = json.loads(data)

Type guard

def is_dict_or_dict_list(data: Any) -> TypeGuard[dict | list[dict]]:
    if isinstance(data, dict):
        return True
    return isinstance(data, list) and all(isinstance(i, dict) for i in data)

Try / catch

try:
    result = validator.run(json=data)
except ValueError as e:
    if "Input must be a dictionary" in str(e):
        data = json.loads(data) if isinstance(data, str) else {}
        result = validator.run(json=data)

Prevention

When it happens

Trigger: Calling JsonSchemaValidator.run with a `json` input that is a bare JSON string, a scalar, or None, instead of a dict or list of dicts. This happens when an upstream LLM component emits the raw string rather than a parsed dict, or when a template/wire step yields None.

Common situations: LLM completion text not parsed to JSON before validation; a generator/extractor component outputs a string; pipeline wiring passes a ChatMessage content string directly; JSON string was json.loads'd at one level but the top-level value remained a string (double-encoded JSON).

Related errors


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/bcac767de2f3da1f. Report an issue: GitHub.