Skip to content

[DNM] Handle allOf and oneOf in json_schema_to_type#3840

Open
strawgate wants to merge 1 commit intomainfrom
fix/json-schema-allof-oneof
Open

[DNM] Handle allOf and oneOf in json_schema_to_type#3840
strawgate wants to merge 1 commit intomainfrom
fix/json-schema-allof-oneof

Conversation

@strawgate
Copy link
Copy Markdown
Collaborator

@strawgate strawgate commented Apr 11, 2026

Summary

json_schema_to_type() handles anyOf but silently returns Any for allOf and oneOf schemas, disabling all validation. This is the most common schema composition pattern in OpenAPI 3.x specs.

allOf (schema intersection) sub-schemas are now merged into a single object type. Properties and required fields from all sub-schemas are combined. $ref sub-schemas are resolved before merging. Nested allOf (including $refallOf) is flattened recursively.

oneOf (exactly-one-of) is now treated like anyOf — creates a Union type. Pydantic's "first match" semantics are a reasonable approximation.

# Before: both return typing.Any
json_schema_to_type({"allOf": [{"type": "object", "properties": {"name": {"type": "string"}}}, ...]})
json_schema_to_type({"oneOf": [{"type": "string"}, {"type": "integer"}]})

# After: proper types with validation
json_schema_to_type({"allOf": [...]})  # → dataclass with merged properties
json_schema_to_type({"oneOf": [...]})  # → Union[str, int]

Known limitations

  • Property constraint merging: if two allOf sub-schemas define the same property with different constraints, the second sub-schema's definition wins (last-writer from dict.update). Merging per-property constraints (e.g., combining minLength from one with maxLength from another) is not implemented. This is rare in practice — the standard pattern uses allOf to combine different properties.

  • Non-object allOf: allOf with scalar sub-schemas that add constraints (e.g., [{type: string}, {minLength: 3}]) doesn't merge the constraints. Object composition (the dominant real-world use case) works correctly.

Test coverage

7 test cases: basic merge, required preservation, $ref resolution, nested allOf, $refallOf, object union, scalar union. 52 lines of source + 169 lines of tests.

Closes #3839

🤖 Generated with Claude Code

@marvin-context-protocol marvin-context-protocol bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. DON'T MERGE PR is not ready for merging. Used by authors to prevent premature merging. openapi Related to OpenAPI integration, parsing, or code generation features. labels Apr 11, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62c3f79d06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +447 to +449
merged: dict[str, Any] = {}
merged_properties: dict[str, Any] = {}
merged_required: list[str] = []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve sibling fields when flattening allOf

The allOf merge path initializes merged/merged_properties from empty dicts and only copies fields from each allOf child, so any sibling properties/required already present on the current schema are dropped. In nested schemas that combine local fields with allOf, this now silently stops validating previously-required local fields (and omits them from the parsed object), which is a regression in validation behavior for composed objects.

Useful? React with 👍 / 👎.

Comment on lines +475 to +476
for subschema in schema["oneOf"]:
types.append(_schema_to_type(subschema, schemas))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep map-like branches intact in oneOf unions

The oneOf loop always calls _schema_to_type(subschema, schemas) directly, unlike anyOf which special-cases object schemas with additionalProperties to produce dict[...]. As a result, a branch like {"type":"object","additionalProperties":{"type":"string"}} becomes an empty dataclass in oneOf, so arbitrary dict inputs validate as Root() and key/value data is discarded instead of being type-checked as a mapping.

Useful? React with 👍 / 👎.

@strawgate strawgate force-pushed the fix/json-schema-allof-oneof branch from 62c3f79 to 4112e18 Compare April 11, 2026 18:34
Previously these compositions silently resolved to Any, disabling
all validation for schemas that use them. allOf is now merged into
a single object schema; oneOf creates a Union type.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@strawgate strawgate force-pushed the fix/json-schema-allof-oneof branch from 4112e18 to 39f8815 Compare April 11, 2026 18:48
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39f88156e5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +453 to +454
if isinstance(sub, bool):
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat false branches in allOf as unsatisfiable

Returning early for boolean sub-schemas silently ignores false inside allOf, but JSON Schema semantics require allOf to fail if any member is false. With the current logic, a schema like {"allOf": [false, {"type":"object", ...}]} still validates inputs against the object branch, so invalid payloads are accepted instead of being rejected.

Useful? React with 👍 / 👎.

Comment on lines +455 to +456
if "$ref" in sub:
sub = dict(_resolve_ref(sub["$ref"], schemas))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid dict-casting boolean refs in allOf flattening

When an allOf member is a $ref to a boolean schema (valid in draft-06+), this code does dict(_resolve_ref(...)), which raises TypeError for true/false targets. That makes json_schema_to_type crash on valid schemas such as {"$defs": {"Never": false}, "allOf": [{"$ref": "#/$defs/Never"}]} instead of producing an unsatisfiable type.

Useful? React with 👍 / 👎.

Comment on lines +470 to +471
merged_properties.update(schema.get("properties", {}))
merged_required.extend(schema.get("required", []))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve top-level additionalProperties in allOf merge

The allOf merge only copies sibling properties/required from the parent schema, so parent-level additionalProperties is dropped. For schemas that combine allOf with additionalProperties: true, this changes behavior from the normal object path (which builds a BaseModel with extras allowed) to a dataclass path that drops unknown keys, causing silent data loss for extra fields.

Useful? React with 👍 / 👎.

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

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. DON'T MERGE PR is not ready for merging. Used by authors to prevent premature merging. openapi Related to OpenAPI integration, parsing, or code generation features.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

json_schema_to_type silently returns Any for allOf and oneOf schemas

1 participant