Skip to content

Fail clearly on unsupported dynamic object schemas - #1835

Open
sylvesterkaczmarek wants to merge 1 commit into
anthropics:mainfrom
sylvesterkaczmarek:fix/structured-output-pattern-maps
Open

Fail clearly on unsupported dynamic object schemas#1835
sylvesterkaczmarek wants to merge 1 commit into
anthropics:mainfrom
sylvesterkaczmarek:fix/structured-output-pattern-maps

Conversation

@sylvesterkaczmarek

Copy link
Copy Markdown

Summary

Prevent transform_schema() from silently turning dynamic-key object schemas into objects that accept no keys.

When an object has no explicit properties and relies on patternProperties, propertyNames, unevaluatedProperties, or an explicit additionalProperties schema to admit keys, the current transformer can demote or discard that information and then force properties: {} plus additionalProperties: false. The transformed schema can therefore only produce {} even though the source schema admits populated dictionaries.

Fixes #1817.

Fix

Detect explicit dynamic-object forms before closing an otherwise property-less object and raise a targeted ValueError instead of returning a silently unsatisfiable schema.

The guard is deliberately narrow:

  • populated patternProperties without explicit properties is rejected;
  • explicit schema-valued additionalProperties without properties is rejected;
  • propertyNames is rejected only while the source object still admits additional keys;
  • non-false unevaluatedProperties without explicit properties is rejected;
  • schemas with explicit properties retain the existing demotion-to-description behavior;
  • already-closed empty objects are not rejected.

Regression coverage

Adds focused tests for the dynamic-key forms, an ordinary Pydantic dict[str, str] field, and controls for explicit-property and already-closed objects.

The patch is confined to Anthropic's hand-maintained src/anthropic/lib/_parse/ transformer and regression tests.

@sylvesterkaczmarek
sylvesterkaczmarek requested a review from a team as a code owner August 16, 2026 22:25
@absol761

Copy link
Copy Markdown

Reviewed this against the repro pattern most likely to actually show up in real Pydantic schemas: Dict[str, Any] (or bare dict), which is probably the most common way people express "arbitrary dict" as a field type.

Pydantic emits that as additionalProperties: True (a bool, not a schema):

class M(BaseModel):
    m: Dict[str, Any]

M.model_json_schema()["properties"]["m"]
# {'additionalProperties': True, 'title': 'M', 'type': 'object'}

_dynamic_object_keywords_without_properties only flags additionalProperties via isinstance(additional_properties, dict), so the True case slips past the guard. With this patch applied, that schema still silently collapses instead of raising:

transform_schema({'additionalProperties': True, 'title': 'M', 'type': 'object'})
# {'type': 'object', 'title': 'M', 'properties': {}, 'additionalProperties': False}

That's the exact bug this PR closes — a dict field turned into a schema that can only ever hold {} — just via a shape the current detector doesn't check. Suggest also treating additional_properties is True as unsupported:

additional_properties = json_schema.get("additionalProperties")
if additional_properties is True or isinstance(additional_properties, dict):
    keywords.append("additionalProperties")

(Verified locally on this branch: applied the diff, imported transform_schema directly, ran the repro above.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

transform_schema empties a patternProperties map: field can only ever be {}

2 participants