From 43512f10d633cb802da6f7d96daaa8ad7de21ce7 Mon Sep 17 00:00:00 2001 From: pomykalakyle Date: Fri, 23 Oct 2026 15:48:46 -0700 Subject: [PATCH 1/2] fix: reuse a model reached twice through a one-option oneOf A oneOf with a single $ref queues the referenced model for processing a second time, so its inline models are built twice and the second build fails as a duplicate class name. Reuse the existing class when the same schema is built again under the same name. --- openapi_python_client/parser/properties/model_property.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 7e3b545e0..d6dca1347 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -113,6 +113,10 @@ def build( python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), example=data.example, ) + existing = schemas.classes_by_name.get(class_info.name) + if isinstance(existing, ModelProperty) and existing.data == data: + # The same schema reached again, e.g. through a one-option oneOf: reuse its class. + return evolve(existing, required=required, name=name, python_name=prop.python_name), schemas if class_info.name in schemas.classes_by_name: error = PropertyError( data=data, detail=f'Attempted to generate duplicate models with name "{class_info.name}"' From 7b923d36d7ac51f449c66c6b4a66ab73d60ca388 Mon Sep 17 00:00:00 2001 From: pomykalakyle Date: Fri, 23 Oct 2026 15:48:46 -0700 Subject: [PATCH 2/2] fix: merge an allOf that narrows a value instead of building a model An allOf whose parts are all values, such as a reference to an enum plus `enum: [ONE_VALUE]`, was built as a model and failed with "Cannot take allOf a non-object". Merge its parts with merge_properties, as allOf already does for model properties. --- .../parser/properties/__init__.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 09aab386b..a20601c8f 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -288,6 +288,16 @@ def property_from_data( # noqa: PLR0911, PLR0912 process_properties=process_properties, roots=roots, ) + if data.allOf and _all_of_is_values(data.allOf, schemas): + return _property_from_values_all_of( + name=name, + required=required, + data=data, + schemas=schemas, + parent_name=parent_name, + config=config, + roots=roots, + ) if data.type == oai.DataType.OBJECT or data.allOf or (data.type is None and data.properties): return ModelProperty.build( data=data, @@ -472,3 +482,70 @@ def build_parameters( parameters.errors.extend(errors) return parameters + + +def _all_of_is_values(parts: list[oai.Reference | oai.Schema], schemas: Schemas) -> bool: + """Whether every part of an allOf describes a value (an enum, string, number...) rather than an object. + + Such an allOf narrows a value, for example a reference to an enum plus `enum: [ONE_VALUE]`. It is merged + property by property instead of being built as a model. + """ + for part in parts: + if isinstance(part, oai.Reference): + ref_path = parse_reference_path(part.ref) + if isinstance(ref_path, ParseError): + return False + existing = schemas.classes_by_reference.get(ref_path) + if existing is None or isinstance(existing, ModelProperty): + return False + continue + if part.type == oai.DataType.OBJECT or part.properties or part.allOf or part.anyOf or part.oneOf: + return False + return True + + +def _property_from_values_all_of( + *, + name: UntrustedString, + required: bool, + data: oai.Schema, + schemas: Schemas, + parent_name: str, + config: Config, + roots: set[ReferencePath | strings.ClassName], +) -> tuple[Property | PropertyError, Schemas]: + """Merge the parts of a value allOf into one property, using the same rules as merging model properties.""" + from .merge_properties import merge_properties # noqa: PLC0415 + + merged: Property | None = None + for part in data.allOf: + prop, schemas = property_from_data( + name=name, + required=required, + data=part, + schemas=schemas, + parent_name=parent_name, + config=config, + roots=roots, + ) + if isinstance(prop, PropertyError): + return prop, schemas + if merged is None: + merged = prop + continue + result = merge_properties(merged, prop) + if isinstance(result, PropertyError): + result.data = data + return result, schemas + merged = result + assert merged is not None + return ( + evolve( + merged, + required=required, + name=name, + python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix), + description=data.description or merged.description, + ), + schemas, + )