zylon-ai/private-gpt · error · ValueError

Expected list or dict with 'items' key, got {type(obj)}

Error message

Expected list or dict with 'items' key, got {type(obj)}

What it means

Raised by the custom model_validate on the dynamic ArrayModel generated from array JSON schemas (create_model_from_json_schema for type=array). The override accepts either a bare Python list (wrapped as items) or a dict containing an "items" key; anything else — a string, number, None, or an items-less dict — raises ValueError with the offending type name.

Source

Thrown at private_gpt/chat/schema_models.py:353

            strict: bool | None = None,
            extra: ExtraValues | None = None,
            from_attributes: bool | None = None,
            context: Any | None = None,
            by_alias: bool | None = None,
            by_name: bool | None = None,
        ) -> Self:
            """Accept array data directly."""
            if isinstance(obj, list):
                return cls(items=obj)
            elif isinstance(obj, dict) and "items" in obj:
                return super().model_validate(
                    obj,
                    strict=strict,
                    from_attributes=from_attributes,
                    context=context,
                )
            else:
                raise ValueError(
                    f"Expected list or dict with 'items' key, got {type(obj)}"
                )

        @classmethod
        def model_json_schema(
            cls,
            by_alias: bool = True,
            ref_template: str = DEFAULT_REF_TEMPLATE,
            schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema,
            mode: JsonSchemaMode = "validation",
            *,
            union_format: Literal["any_of", "primitive_type_array"] = "any_of",
        ) -> dict[str, Any]:
            """Return the original array schema, not wrapped in object schema."""
            return schema

        model_config = ConfigDict(populate_by_name=True, use_attribute_docstrings=True)

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Parse JSON text first, then validate: ArrayModel.model_validate(json.loads(raw)).
  2. Pass a bare list directly: ArrayModel.model_validate([1, 2, 3]) — the override wraps it.
  3. If using the dict form, keep the 'items' key exactly (or the configured alias) — model_dump_json round-trips with it.
  4. Check the reported type in the message ({type(obj)}) to identify what actually arrived.

Example fix

// before
model = ArrayModel.model_validate(raw_llm_output)  # raw is a str

// after
import json
model = ArrayModel.model_validate(json.loads(raw_llm_output))
Defensive patterns

Strategy: try-catch

Validate before calling

import json

if isinstance(data, (str, bytes)):
    data = json.loads(data)
if isinstance(data, dict) and "items" not in data and "values" not in data:
    data = list(data.values())[0] if len(data) == 1 else data

Type guard

def is_array_model_input(obj: object) -> bool:
    return isinstance(obj, list) or (isinstance(obj, dict) and "items" in obj)

Try / catch

try:
    model = ArrayModel.model_validate(payload)
except ValueError:
    parsed = json.loads(payload) if isinstance(payload, str) else payload
    model = ArrayModel.model_validate(parsed)

Prevention

When it happens

Trigger: Calling ArrayModel.model_validate(json_string) where json_string is a str like '[1,2]'; model_validate({"values": [...]}) (dict without 'items'); model_validate(None) or model_validate(42). Happens when structured chat output is parsed into the dynamically generated array model.

Common situations: Feeding raw LLM JSON text responses into model_validate without json.loads; renaming the wrapper key in serialized output (dumping by items alias off); validating objects produced by a different schema.

Related errors


AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15). Data as JSON: /api/errors/de0013e1bce216ae. Report an issue: GitHub.