Skip to content

json(schema=...) rejects bare dict / Dict[Any, ...] fields as 'non-string keys', but they map to a valid, supported object schema #1493

Description

@ErenAta16

Describe the bug

json(schema=...) rejects a pydantic model (or TypeAdapter) with a bare dict / Dict[Any, ...] field, raising TypeError: JSON does not support non-string keys, got type any. But a bare dict maps to an unconstrained JSON object, which is valid (JSON object keys are always strings) and is actually supported by the backend. The pre-check in GenerateJsonSchemaSafe is stricter than it should be.

Where

guidance/library/_pydantic.py, GenerateJsonSchemaSafe.generate_inner:

def generate_inner(self, schema):
    if schema["type"] == "dict":
        key_type = schema["keys_schema"]["type"]
        if key_type != "str":
            raise TypeError(f"JSON does not support non-string keys, got type {key_type}")
    return super().generate_inner(schema)

The check is meant to catch genuinely non-string keys such as dict[int, ...], which it does correctly. But a dict with no key type (bare dict, Dict[Any, Any], Dict[str, Any] with the key unparametrized to Any) has a keys_schema of {"type": "any"}, and "any" != "str", so it is rejected too. That is a false positive: an any-keyed dict is a plain JSON object.

To Reproduce

from pydantic import BaseModel
from guidance import json

# common "arbitrary JSON metadata" pattern
class Config(BaseModel):
    name: str
    metadata: dict

json(schema=Config)
# TypeError: JSON does not support non-string keys, got type any

Why it's a false positive

Pydantic's own default schema generator turns a bare dict into a valid, unconstrained object schema, and the backend accepts it:

from pydantic import TypeAdapter
from guidance import json

print(TypeAdapter(dict).json_schema())
# {'additionalProperties': True, 'type': 'object'}

json(schema={'type': 'object'})            # accepted, llguidance validates fine
json(schema=TypeAdapter(dict).json_schema())  # accepted

So the schema a bare dict produces is supported end to end; it is only the GenerateJsonSchemaSafe pre-check that blocks it, and the error message ("JSON does not support non-string keys") is inaccurate for this case: a bare dict's keys are strings once serialized, they are just unconstrained.

dict[int, int] is correctly still rejected, that part of the check is doing its job.

Suggested fix

Allow the "any" key type alongside "str", since an any-keyed dict is a valid JSON object:

key_type = schema.get("keys_schema", {}).get("type", "any")
if key_type not in ("str", "any"):
    raise TypeError(f"JSON does not support non-string keys, got type {key_type}")

int/float/etc. keys stay rejected. I can open a PR with this plus tests (bare dict, Dict[str, Any], Dict[Any, Any], and a bare-dict model field are accepted; dict[int, int] still raises).

Related, not included in the fix

dict[Literal["x", "y"], int] (string-literal keys) is also rejected with the same "non-string keys, got type literal" message. Those keys are strings, but pydantic represents that schema with propertyNames, which the backend reports as Unimplemented keys: ["propertyNames"]. So that case is genuinely unsupported today, just for a different reason than the message states. Flagging it here for visibility; the fix above deliberately leaves literal keys rejected rather than pushing an unsupported schema downstream.

System info

Reproduced against guidance-ai/guidance main (current). Pure Python, models.Mock / schema-validation path, no real model needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions