-
Notifications
You must be signed in to change notification settings - Fork 2k
fix(json_schema_to_type): add allOf and oneOf handling #4019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -380,6 +380,56 @@ def _return_Any() -> Any: | |
| return Any | ||
|
|
||
|
|
||
| def _merge_object_types(types: list[type]) -> type | None: | ||
| """Try to merge multiple object types into one. | ||
|
|
||
| For allOf schemas where all sub-schemas are objects/dataclasses, | ||
| we can create a merged dataclass with combined properties. | ||
| Returns None if types can't be meaningfully merged. | ||
| """ | ||
| from dataclasses import Field as DataclassField, make_dataclass | ||
| from typing import get_origin, get_args, Union | ||
|
|
||
| # Filter to only types that have __annotations__ (dataclass-like) | ||
| object_types = [] | ||
| for t in types: | ||
| origin = get_origin(t) | ||
| if origin is None and hasattr(t, "__annotations__"): | ||
| object_types.append(t) | ||
| elif origin is Union: | ||
| # Unwrap Union types to find object types | ||
| for arg in get_args(t): | ||
| if hasattr(arg, "__annotations__"): | ||
| object_types.append(arg) | ||
|
|
||
| if len(object_types) < 2: | ||
| return None | ||
|
|
||
| # Merge all properties from object types | ||
| merged_fields: dict[str, tuple[type, DataclassField]] = {} | ||
| for obj_type in object_types: | ||
| for field_name, field_type in obj_type.__annotations__.items(): | ||
| if field_name not in merged_fields: | ||
| # Get default from the field | ||
| if hasattr(obj_type, "__dataclass_fields__"): | ||
| dc_field = obj_type.__dataclass_fields__.get(field_name) | ||
| if dc_field and dc_field.default is not DataclassField.MISSING: | ||
| merged_fields[field_name] = (field_type, field(default=dc_field.default)) | ||
| elif dc_field and dc_field.default_factory is not DataclassField.MISSING: | ||
| merged_fields[field_name] = (field_type, field(default_factory=dc_field.default_factory)) | ||
| else: | ||
| merged_fields[field_name] = (field_type, field()) | ||
| else: | ||
| merged_fields[field_name] = (field_type, field()) | ||
|
|
||
| if not merged_fields: | ||
| return None | ||
|
|
||
| # Create merged dataclass | ||
| field_specs = [(name, ftype, fdef) for name, (ftype, fdef) in merged_fields.items()] | ||
| return make_dataclass("MergedAllOf", field_specs, kw_only=True) | ||
|
|
||
|
|
||
| def _object_schema_to_type( | ||
| schema: Mapping[str, Any], | ||
| schemas: Mapping[str, Any], | ||
|
|
@@ -470,6 +520,34 @@ def _schema_to_type( | |
| if "enum" in schema: | ||
| return _create_enum(f"Enum_{len(_classes)}", schema["enum"]) | ||
|
|
||
| # Handle oneOf unions (similar to anyOf but with exactly-one semantics) | ||
| if "oneOf" in schema: | ||
| types: list[type | Any] = [ | ||
| _schema_to_type(subschema, schemas) for subschema in schema["oneOf"] | ||
| ] | ||
| types = [t for t in types if t is not type(None)] | ||
| if len(types) == 0: | ||
| return type(None) | ||
| elif len(types) == 1: | ||
| return types[0] | ||
|
Comment on lines
+528
to
+532
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Useful? React with 👍 / 👎. |
||
| else: | ||
| return Union[tuple(types)] # type: ignore # noqa: UP007 | ||
|
|
||
| # Handle allOf (intersection - value must satisfy ALL sub-schemas) | ||
| if "allOf" in schema: | ||
| types: list[type | Any] = [ | ||
| _schema_to_type(subschema, schemas) for subschema in schema["allOf"] | ||
| ] | ||
| types = [t for t in types if t is not type(None)] | ||
| if len(types) == 0: | ||
| return type(None) | ||
| elif len(types) == 1: | ||
| return types[0] | ||
| else: | ||
| # For allOf intersection, try to merge object types | ||
| merged = _merge_object_types(types) | ||
| return merged if merged is not None else Union[tuple(types)] # type: ignore # noqa: UP007 | ||
|
|
||
| # Handle anyOf unions | ||
| if "anyOf" in schema: | ||
| types: list[type | Any] = [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking
dc_field.defaultanddc_field.default_factoryagainstDataclassField.MISSINGraisesAttributeErrorbecauseFieldhas noMISSINGattribute. AnyallOfschema that reaches_merge_object_types()with dataclass-backed object branches will crash during type generation instead of returning a merged type.Useful? React with 👍 / 👎.