Skip to content

Commit 25161b6

Browse files
committed
Allow bare dict / Dict[Any, ...] fields in json schema generation
GenerateJsonSchemaSafe rejected any dict whose key type wasn't exactly 'str', which also caught bare dict / Dict[Any, ...] fields (keys_schema type 'any'). Those map to an unconstrained JSON object, which is valid (JSON object keys are always strings) and is accepted by the backend, so rejecting them with 'JSON does not support non-string keys' is a false positive with a misleading message. Allow the 'any' key type alongside 'str'; int/float/etc. keys stay rejected. Also read keys_schema/type defensively via .get(). Adds tests for bare dict, Dict[str, Any], Dict[Any, Any], and a bare-dict model field being accepted, and keeps the existing int-key rejection test. Closes #1493
1 parent 21b1d90 commit 25161b6

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

guidance/library/_pydantic.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ class GenerateJsonSchemaSafe(pydantic.json_schema.GenerateJsonSchema):
1717

1818
def generate_inner(self, schema):
1919
if schema["type"] == "dict":
20-
key_type = schema["keys_schema"]["type"]
21-
if key_type != "str":
20+
# A dict with no key type (bare ``dict``, ``Dict[Any, ...]``) has an
21+
# ``"any"`` keys_schema. In JSON that maps to an unconstrained object
22+
# (JSON object keys are always strings), which is a valid, supported
23+
# schema, so it must not be rejected as "non-string keys". Only key
24+
# types that genuinely can't be represented as JSON strings (``int``,
25+
# ``float``, ...) are rejected.
26+
key_type = schema.get("keys_schema", {}).get("type", "any")
27+
if key_type not in ("str", "any"):
2228
raise TypeError(f"JSON does not support non-string keys, got type {key_type}")
2329
return super().generate_inner(schema)
2430

tests/unit/library/test_pydantic.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,28 @@ def test_prevent_non_string_keys(self):
170170
generate_and_check({1: 2}, model)
171171
assert exc_info.value.args[0] == "JSON does not support non-string keys, got type int"
172172

173+
def test_any_keys_allowed(self):
174+
"""
175+
A dict with no key type (bare ``dict``, ``Dict[Any, ...]``) maps to an
176+
unconstrained JSON object and must not be rejected as "non-string keys":
177+
JSON object keys are always strings, and the resulting ``{"type":
178+
"object"}`` schema is supported by the backend.
179+
"""
180+
for model in (
181+
pydantic.TypeAdapter(dict),
182+
pydantic.TypeAdapter(dict[str, Any]),
183+
pydantic.TypeAdapter(dict[Any, Any]),
184+
):
185+
generate_and_check({"a": 1, "b": 2}, model)
186+
187+
def test_any_keys_allowed_as_model_field(self):
188+
# The common "arbitrary JSON metadata" pattern: a bare dict field.
189+
class Config(pydantic.BaseModel):
190+
name: str
191+
metadata: dict
192+
193+
generate_and_check({"name": "x", "metadata": {"k": 1}}, Config)
194+
173195

174196
class TestComposite:
175197
class Simple(pydantic.BaseModel):

0 commit comments

Comments
 (0)