From 9056e92c01e00805e7fe1c68fbac08905625ca6f Mon Sep 17 00:00:00 2001
From: Eli Bishop <eli.bishop@benchling.com>
Date: Fri, 15 Nov 2024 12:26:22 -0800
Subject: [PATCH 1/5] fix bug in type generation for nullable props BNCH-111776

---
 .../test_enums_and_consts.py                  | 58 ++++++++++++++++---
 .../my_test_api_client/models/__init__.py     | 10 ++--
 .../my_test_api_client/models/a_model.py      |  8 +--
 .../body_upload_file_tests_upload_post.py     |  4 +-
 .../my_test_api_client/models/extended.py     |  8 +--
 .../models/model_with_any_json_properties.py  | 30 +++++-----
 ...ny_json_properties_additional_property.py} | 10 ++--
 ...ions_simple_before_complex_response_200.py | 26 ++++-----
 ...ns_simple_before_complex_response_200a.py} | 10 ++--
 .../models/post_user_list_body.py             |  4 +-
 .../parser/properties/enum_property.py        |  4 +-
 .../parser/properties/model_property.py       |  4 +-
 .../parser/properties/protocol.py             | 13 ++++-
 .../parser/properties/union.py                | 50 +++++++++++-----
 14 files changed, 155 insertions(+), 84 deletions(-)
 rename end_to_end_tests/golden-record/my_test_api_client/models/{model_with_any_json_properties_additional_property_type_0.py => model_with_any_json_properties_additional_property.py} (82%)
 rename end_to_end_tests/golden-record/my_test_api_client/models/{post_responses_unions_simple_before_complex_response_200a_type_1.py => post_responses_unions_simple_before_complex_response_200a.py} (89%)

diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py
index 89dbef7dc..c612813a0 100644
--- a/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py
+++ b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py
@@ -152,28 +152,70 @@ def test_invalid_values(self, MyModel):
 """)
 @with_generated_code_imports(
     ".models.MyEnum",
-    ".models.MyEnumIncludingNullType1", # see comment in test_nullable_enum_prop
+    ".models.MyEnumIncludingNull",
     ".models.MyModel",
     ".types.Unset",
 )
 class TestNullableEnums:
-    def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNullType1):
-        # Note, MyEnumIncludingNullType1 should be named just MyEnumIncludingNull -
-        # known bug: https://github.com/openapi-generators/openapi-python-client/issues/1120
+    def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNull):
         assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
         assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
         assert_model_decode_encode(
             MyModel,
             {"enumIncludingNullProp": "a"},
-            MyModel(enum_including_null_prop=MyEnumIncludingNullType1.A),
+            MyModel(enum_including_null_prop=MyEnumIncludingNull.A),
         )
         assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None))
         assert_model_decode_encode(MyModel, {"nullOnlyEnumProp": None}, MyModel(null_only_enum_prop=None))
     
-    def test_type_hints(self, MyModel, MyEnum, Unset):
-        expected_type = Union[MyEnum, None, Unset]
-        assert_model_property_type_hint(MyModel, "nullable_enum_prop", expected_type)
+    def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
+        assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
+        assert_model_property_type_hint(MyModel, "enum_including_null_prop", Union[MyEnumIncludingNull, None, Unset])
+        assert_model_property_type_hint(MyModel, "null_only_enum_prop", Union[None, Unset])
+
+
+@with_generated_client_fixture(
+"""
+openapi: 3.0.0
+
+components:
+  schemas:
+    MyEnum:
+      type: string
+      enum: ["a", "b"]
+    MyEnumIncludingNull:
+      type: string
+      nullable: true
+      enum: ["a", "b", null]
+    MyModel:
+      properties:
+        nullableEnumProp:
+          allOf:
+            - {"$ref": "#/components/schemas/MyEnum"}
+          nullable: true
+        enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
+""")
+@with_generated_code_imports(
+    ".models.MyEnum",
+    ".models.MyEnumIncludingNull",
+    ".models.MyModel",
+    ".types.Unset",
+)
+class TestNullableEnumsInOpenAPI30:
+    def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNull):
+        assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
+        assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
+        assert_model_decode_encode(
+            MyModel,
+            {"enumIncludingNullProp": "a"},
+            MyModel(enum_including_null_prop=MyEnumIncludingNull.A),
+        )
+        assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None))
     
+    def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
+        assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
+        assert_model_property_type_hint(MyModel, "enum_including_null_prop", Union[MyEnumIncludingNull, None, Unset])
+
 
 @with_generated_client_fixture(
 """
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py
index 6ba623039..06b8a1d9f 100644
--- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py
@@ -52,7 +52,7 @@
 )
 from .model_with_additional_properties_refed import ModelWithAdditionalPropertiesRefed
 from .model_with_any_json_properties import ModelWithAnyJsonProperties
-from .model_with_any_json_properties_additional_property_type_0 import ModelWithAnyJsonPropertiesAdditionalPropertyType0
+from .model_with_any_json_properties_additional_property import ModelWithAnyJsonPropertiesAdditionalProperty
 from .model_with_backslash_in_description import ModelWithBackslashInDescription
 from .model_with_circular_ref_a import ModelWithCircularRefA
 from .model_with_circular_ref_b import ModelWithCircularRefB
@@ -79,8 +79,8 @@
 from .post_naming_property_conflict_with_import_body import PostNamingPropertyConflictWithImportBody
 from .post_naming_property_conflict_with_import_response_200 import PostNamingPropertyConflictWithImportResponse200
 from .post_responses_unions_simple_before_complex_response_200 import PostResponsesUnionsSimpleBeforeComplexResponse200
-from .post_responses_unions_simple_before_complex_response_200a_type_1 import (
-    PostResponsesUnionsSimpleBeforeComplexResponse200AType1,
+from .post_responses_unions_simple_before_complex_response_200a import (
+    PostResponsesUnionsSimpleBeforeComplexResponse200A,
 )
 from .test_inline_objects_body import TestInlineObjectsBody
 from .test_inline_objects_response_200 import TestInlineObjectsResponse200
@@ -131,7 +131,7 @@
     "ModelWithAdditionalPropertiesInlinedAdditionalProperty",
     "ModelWithAdditionalPropertiesRefed",
     "ModelWithAnyJsonProperties",
-    "ModelWithAnyJsonPropertiesAdditionalPropertyType0",
+    "ModelWithAnyJsonPropertiesAdditionalProperty",
     "ModelWithBackslashInDescription",
     "ModelWithCircularRefA",
     "ModelWithCircularRefB",
@@ -158,7 +158,7 @@
     "PostNamingPropertyConflictWithImportBody",
     "PostNamingPropertyConflictWithImportResponse200",
     "PostResponsesUnionsSimpleBeforeComplexResponse200",
-    "PostResponsesUnionsSimpleBeforeComplexResponse200AType1",
+    "PostResponsesUnionsSimpleBeforeComplexResponse200A",
     "TestInlineObjectsBody",
     "TestInlineObjectsResponse200",
     "ValidationError",
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py
index a14400c9d..774642622 100644
--- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py
@@ -373,9 +373,9 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None]
             try:
                 if not isinstance(data, dict):
                     raise TypeError()
-                nullable_model_type_1 = ModelWithUnionProperty.from_dict(data)
+                nullable_model = ModelWithUnionProperty.from_dict(data)
 
-                return nullable_model_type_1
+                return nullable_model
             except:  # noqa: E722
                 pass
             return cast(Union["ModelWithUnionProperty", None], data)
@@ -495,9 +495,9 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro
             try:
                 if not isinstance(data, dict):
                     raise TypeError()
-                not_required_nullable_model_type_1 = ModelWithUnionProperty.from_dict(data)
+                not_required_nullable_model = ModelWithUnionProperty.from_dict(data)
 
-                return not_required_nullable_model_type_1
+                return not_required_nullable_model
             except:  # noqa: E722
                 pass
             return cast(Union["ModelWithUnionProperty", None, Unset], data)
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py
index d7fbbf835..9c1a1e7e6 100644
--- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py
@@ -304,9 +304,9 @@ def _parse_some_nullable_object(data: object) -> Union["BodyUploadFileTestsUploa
             try:
                 if not isinstance(data, dict):
                     raise TypeError()
-                some_nullable_object_type_0 = BodyUploadFileTestsUploadPostSomeNullableObject.from_dict(data)
+                some_nullable_object = BodyUploadFileTestsUploadPostSomeNullableObject.from_dict(data)
 
-                return some_nullable_object_type_0
+                return some_nullable_object
             except:  # noqa: E722
                 pass
             return cast(Union["BodyUploadFileTestsUploadPostSomeNullableObject", None], data)
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py
index 324513d3a..b51540cab 100644
--- a/end_to_end_tests/golden-record/my_test_api_client/models/extended.py
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/extended.py
@@ -381,9 +381,9 @@ def _parse_nullable_model(data: object) -> Union["ModelWithUnionProperty", None]
             try:
                 if not isinstance(data, dict):
                     raise TypeError()
-                nullable_model_type_1 = ModelWithUnionProperty.from_dict(data)
+                nullable_model = ModelWithUnionProperty.from_dict(data)
 
-                return nullable_model_type_1
+                return nullable_model
             except:  # noqa: E722
                 pass
             return cast(Union["ModelWithUnionProperty", None], data)
@@ -503,9 +503,9 @@ def _parse_not_required_nullable_model(data: object) -> Union["ModelWithUnionPro
             try:
                 if not isinstance(data, dict):
                     raise TypeError()
-                not_required_nullable_model_type_1 = ModelWithUnionProperty.from_dict(data)
+                not_required_nullable_model = ModelWithUnionProperty.from_dict(data)
 
-                return not_required_nullable_model_type_1
+                return not_required_nullable_model
             except:  # noqa: E722
                 pass
             return cast(Union["ModelWithUnionProperty", None, Unset], data)
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py
index 6e669914a..89498e3bd 100644
--- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties.py
@@ -4,9 +4,7 @@
 from attrs import field as _attrs_field
 
 if TYPE_CHECKING:
-    from ..models.model_with_any_json_properties_additional_property_type_0 import (
-        ModelWithAnyJsonPropertiesAdditionalPropertyType0,
-    )
+    from ..models.model_with_any_json_properties_additional_property import ModelWithAnyJsonPropertiesAdditionalProperty
 
 
 T = TypeVar("T", bound="ModelWithAnyJsonProperties")
@@ -17,17 +15,17 @@ class ModelWithAnyJsonProperties:
     """ """
 
     additional_properties: Dict[
-        str, Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str]
+        str, Union["ModelWithAnyJsonPropertiesAdditionalProperty", List[str], bool, float, int, str]
     ] = _attrs_field(init=False, factory=dict)
 
     def to_dict(self) -> Dict[str, Any]:
-        from ..models.model_with_any_json_properties_additional_property_type_0 import (
-            ModelWithAnyJsonPropertiesAdditionalPropertyType0,
+        from ..models.model_with_any_json_properties_additional_property import (
+            ModelWithAnyJsonPropertiesAdditionalProperty,
         )
 
         field_dict: Dict[str, Any] = {}
         for prop_name, prop in self.additional_properties.items():
-            if isinstance(prop, ModelWithAnyJsonPropertiesAdditionalPropertyType0):
+            if isinstance(prop, ModelWithAnyJsonPropertiesAdditionalProperty):
                 field_dict[prop_name] = prop.to_dict()
             elif isinstance(prop, list):
                 field_dict[prop_name] = prop
@@ -39,8 +37,8 @@ def to_dict(self) -> Dict[str, Any]:
 
     @classmethod
     def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
-        from ..models.model_with_any_json_properties_additional_property_type_0 import (
-            ModelWithAnyJsonPropertiesAdditionalPropertyType0,
+        from ..models.model_with_any_json_properties_additional_property import (
+            ModelWithAnyJsonPropertiesAdditionalProperty,
         )
 
         d = src_dict.copy()
@@ -51,13 +49,13 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
 
             def _parse_additional_property(
                 data: object,
-            ) -> Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str]:
+            ) -> Union["ModelWithAnyJsonPropertiesAdditionalProperty", List[str], bool, float, int, str]:
                 try:
                     if not isinstance(data, dict):
                         raise TypeError()
-                    additional_property_type_0 = ModelWithAnyJsonPropertiesAdditionalPropertyType0.from_dict(data)
+                    additional_property = ModelWithAnyJsonPropertiesAdditionalProperty.from_dict(data)
 
-                    return additional_property_type_0
+                    return additional_property
                 except:  # noqa: E722
                     pass
                 try:
@@ -69,7 +67,7 @@ def _parse_additional_property(
                 except:  # noqa: E722
                     pass
                 return cast(
-                    Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str], data
+                    Union["ModelWithAnyJsonPropertiesAdditionalProperty", List[str], bool, float, int, str], data
                 )
 
             additional_property = _parse_additional_property(prop_dict)
@@ -85,13 +83,11 @@ def additional_keys(self) -> List[str]:
 
     def __getitem__(
         self, key: str
-    ) -> Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str]:
+    ) -> Union["ModelWithAnyJsonPropertiesAdditionalProperty", List[str], bool, float, int, str]:
         return self.additional_properties[key]
 
     def __setitem__(
-        self,
-        key: str,
-        value: Union["ModelWithAnyJsonPropertiesAdditionalPropertyType0", List[str], bool, float, int, str],
+        self, key: str, value: Union["ModelWithAnyJsonPropertiesAdditionalProperty", List[str], bool, float, int, str]
     ) -> None:
         self.additional_properties[key] = value
 
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property.py
similarity index 82%
rename from end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py
rename to end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property.py
index 6ae70905e..cd6cc851a 100644
--- a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property_type_0.py
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_any_json_properties_additional_property.py
@@ -3,11 +3,11 @@
 from attrs import define as _attrs_define
 from attrs import field as _attrs_field
 
-T = TypeVar("T", bound="ModelWithAnyJsonPropertiesAdditionalPropertyType0")
+T = TypeVar("T", bound="ModelWithAnyJsonPropertiesAdditionalProperty")
 
 
 @_attrs_define
-class ModelWithAnyJsonPropertiesAdditionalPropertyType0:
+class ModelWithAnyJsonPropertiesAdditionalProperty:
     """ """
 
     additional_properties: Dict[str, str] = _attrs_field(init=False, factory=dict)
@@ -21,10 +21,10 @@ def to_dict(self) -> Dict[str, Any]:
     @classmethod
     def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
         d = src_dict.copy()
-        model_with_any_json_properties_additional_property_type_0 = cls()
+        model_with_any_json_properties_additional_property = cls()
 
-        model_with_any_json_properties_additional_property_type_0.additional_properties = d
-        return model_with_any_json_properties_additional_property_type_0
+        model_with_any_json_properties_additional_property.additional_properties = d
+        return model_with_any_json_properties_additional_property
 
     @property
     def additional_keys(self) -> List[str]:
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py b/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py
index 0b6a29243..3c07edb03 100644
--- a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200.py
@@ -4,8 +4,8 @@
 from attrs import field as _attrs_field
 
 if TYPE_CHECKING:
-    from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import (
-        PostResponsesUnionsSimpleBeforeComplexResponse200AType1,
+    from ..models.post_responses_unions_simple_before_complex_response_200a import (
+        PostResponsesUnionsSimpleBeforeComplexResponse200A,
     )
 
 
@@ -16,19 +16,19 @@
 class PostResponsesUnionsSimpleBeforeComplexResponse200:
     """
     Attributes:
-        a (Union['PostResponsesUnionsSimpleBeforeComplexResponse200AType1', str]):
+        a (Union['PostResponsesUnionsSimpleBeforeComplexResponse200A', str]):
     """
 
-    a: Union["PostResponsesUnionsSimpleBeforeComplexResponse200AType1", str]
+    a: Union["PostResponsesUnionsSimpleBeforeComplexResponse200A", str]
     additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict)
 
     def to_dict(self) -> Dict[str, Any]:
-        from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import (
-            PostResponsesUnionsSimpleBeforeComplexResponse200AType1,
+        from ..models.post_responses_unions_simple_before_complex_response_200a import (
+            PostResponsesUnionsSimpleBeforeComplexResponse200A,
         )
 
         a: Union[Dict[str, Any], str]
-        if isinstance(self.a, PostResponsesUnionsSimpleBeforeComplexResponse200AType1):
+        if isinstance(self.a, PostResponsesUnionsSimpleBeforeComplexResponse200A):
             a = self.a.to_dict()
         else:
             a = self.a
@@ -45,22 +45,22 @@ def to_dict(self) -> Dict[str, Any]:
 
     @classmethod
     def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
-        from ..models.post_responses_unions_simple_before_complex_response_200a_type_1 import (
-            PostResponsesUnionsSimpleBeforeComplexResponse200AType1,
+        from ..models.post_responses_unions_simple_before_complex_response_200a import (
+            PostResponsesUnionsSimpleBeforeComplexResponse200A,
         )
 
         d = src_dict.copy()
 
-        def _parse_a(data: object) -> Union["PostResponsesUnionsSimpleBeforeComplexResponse200AType1", str]:
+        def _parse_a(data: object) -> Union["PostResponsesUnionsSimpleBeforeComplexResponse200A", str]:
             try:
                 if not isinstance(data, dict):
                     raise TypeError()
-                a_type_1 = PostResponsesUnionsSimpleBeforeComplexResponse200AType1.from_dict(data)
+                a = PostResponsesUnionsSimpleBeforeComplexResponse200A.from_dict(data)
 
-                return a_type_1
+                return a
             except:  # noqa: E722
                 pass
-            return cast(Union["PostResponsesUnionsSimpleBeforeComplexResponse200AType1", str], data)
+            return cast(Union["PostResponsesUnionsSimpleBeforeComplexResponse200A", str], data)
 
         a = _parse_a(d.pop("a"))
 
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py b/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a.py
similarity index 89%
rename from end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py
rename to end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a.py
index 601d17cf8..a59c0962a 100644
--- a/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a_type_1.py
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/post_responses_unions_simple_before_complex_response_200a.py
@@ -3,11 +3,11 @@
 from attrs import define as _attrs_define
 from attrs import field as _attrs_field
 
-T = TypeVar("T", bound="PostResponsesUnionsSimpleBeforeComplexResponse200AType1")
+T = TypeVar("T", bound="PostResponsesUnionsSimpleBeforeComplexResponse200A")
 
 
 @_attrs_define
-class PostResponsesUnionsSimpleBeforeComplexResponse200AType1:
+class PostResponsesUnionsSimpleBeforeComplexResponse200A:
     """ """
 
     additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict)
@@ -21,10 +21,10 @@ def to_dict(self) -> Dict[str, Any]:
     @classmethod
     def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
         d = src_dict.copy()
-        post_responses_unions_simple_before_complex_response_200a_type_1 = cls()
+        post_responses_unions_simple_before_complex_response_200a = cls()
 
-        post_responses_unions_simple_before_complex_response_200a_type_1.additional_properties = d
-        return post_responses_unions_simple_before_complex_response_200a_type_1
+        post_responses_unions_simple_before_complex_response_200a.additional_properties = d
+        return post_responses_unions_simple_before_complex_response_200a
 
     @property
     def additional_keys(self) -> List[str]:
diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py
index e61cb4183..db13a1e93 100644
--- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py
+++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py
@@ -187,9 +187,9 @@ def _parse_an_enum_value_with_null_item(data: object) -> Union[AnEnumWithNull, N
                 try:
                     if not isinstance(data, str):
                         raise TypeError()
-                    componentsschemas_an_enum_with_null_type_1 = check_an_enum_with_null(data)
+                    componentsschemas_an_enum_with_null = check_an_enum_with_null(data)
 
-                    return componentsschemas_an_enum_with_null_type_1
+                    return componentsschemas_an_enum_with_null
                 except:  # noqa: E722
                     pass
                 return cast(Union[AnEnumWithNull, None], data)
diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py
index 29609864f..07c815899 100644
--- a/openapi_python_client/parser/properties/enum_property.py
+++ b/openapi_python_client/parser/properties/enum_property.py
@@ -12,7 +12,7 @@
 from ...schema import DataType
 from ..errors import PropertyError
 from .none import NoneProperty
-from .protocol import PropertyProtocol, Value
+from .protocol import HasNamedClass, PropertyProtocol, Value
 from .schemas import Class, Schemas
 from .union import UnionProperty
 
@@ -20,7 +20,7 @@
 
 
 @define
-class EnumProperty(PropertyProtocol):
+class EnumProperty(PropertyProtocol, HasNamedClass):
     """A property that should use an enum"""
 
     name: str
diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py
index fd3e24ac8..c0846f2d1 100644
--- a/openapi_python_client/parser/properties/model_property.py
+++ b/openapi_python_client/parser/properties/model_property.py
@@ -10,12 +10,12 @@
 from ...utils import PythonIdentifier
 from ..errors import ParseError, PropertyError
 from .any import AnyProperty
-from .protocol import PropertyProtocol, Value
+from .protocol import HasNamedClass, PropertyProtocol, Value
 from .schemas import Class, ReferencePath, Schemas, parse_reference_path
 
 
 @define
-class ModelProperty(PropertyProtocol):
+class ModelProperty(PropertyProtocol, HasNamedClass):
     """A property which refers to another Schema"""
 
     name: str
diff --git a/openapi_python_client/parser/properties/protocol.py b/openapi_python_client/parser/properties/protocol.py
index 17b55c3f1..27f9dcd30 100644
--- a/openapi_python_client/parser/properties/protocol.py
+++ b/openapi_python_client/parser/properties/protocol.py
@@ -6,12 +6,13 @@
 
 from abc import abstractmethod
 from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar
+from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar, runtime_checkable
 
 from ... import Config
 from ... import schema as oai
 from ...utils import PythonIdentifier
 from ..errors import ParseError, PropertyError
+from .schemas import Class
 
 if TYPE_CHECKING:  # pragma: no cover
     from .model_property import ModelProperty
@@ -190,3 +191,13 @@ def is_base_type(self) -> bool:
 
     def get_ref_path(self) -> ReferencePath | None:
         return self.ref_path if hasattr(self, "ref_path") else None
+
+
+@runtime_checkable
+class HasNamedClass(Protocol):
+    """
+    This protocol is implemented by any property types that will have a corresponding Python
+    class in the generated code. Currently that is ModelProperty and UnionProperty.
+    """
+
+    class_info: Class
\ No newline at end of file
diff --git a/openapi_python_client/parser/properties/union.py b/openapi_python_client/parser/properties/union.py
index efa48eda2..574b44c7f 100644
--- a/openapi_python_client/parser/properties/union.py
+++ b/openapi_python_client/parser/properties/union.py
@@ -9,7 +9,7 @@
 from ... import schema as oai
 from ...utils import PythonIdentifier
 from ..errors import ParseError, PropertyError
-from .protocol import PropertyProtocol, Value
+from .protocol import HasNamedClass, PropertyProtocol, Value
 from .schemas import Schemas, get_reference_simple_name, parse_reference_path
 
 
@@ -80,22 +80,44 @@ def build(
         sub_properties: list[PropertyProtocol] = []
 
         type_list_data = []
-        if isinstance(data.type, list):
+        if isinstance(data.type, list) and not (data.anyOf or data.oneOf):
             for _type in data.type:
                 type_list_data.append(data.model_copy(update={"type": _type, "default": None}))
 
-        for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf, type_list_data)):
-            sub_prop, schemas = property_from_data(
-                name=f"{name}_type_{i}",
-                required=True,
-                data=sub_prop_data,
-                schemas=schemas,
-                parent_name=parent_name,
-                config=config,
-            )
-            if isinstance(sub_prop, PropertyError):
-                return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas
-            sub_properties.append(sub_prop)
+        def process_items(
+            preserve_name_for_item: oai.Schema | oai.Reference | None = None,
+        ) -> tuple[list[PropertyProtocol] | PropertyError, Schemas]:
+            props: list[PropertyProtocol] = []
+            new_schemas = schemas
+            items_with_classes: list[oai.Schema | oai.Reference] = []
+            for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf, type_list_data)):
+                sub_prop_name = name if sub_prop_data is preserve_name_for_item else f"{name}_type_{i}"
+                sub_prop, new_schemas = property_from_data(
+                    name=sub_prop_name,
+                    required=True,
+                    data=sub_prop_data,
+                    schemas=new_schemas,
+                    parent_name=parent_name,
+                    config=config,
+                )
+                if isinstance(sub_prop, PropertyError):
+                    return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), new_schemas
+                if isinstance(sub_prop, HasNamedClass):
+                    items_with_classes.append(sub_prop_data)
+                props.append(sub_prop)
+
+            if (not preserve_name_for_item) and (len(items_with_classes) == 1):
+                # After our first pass, if it turns out that there was exactly one enum or model in the list,
+                # then we'll do a second pass where we use the original name for that item instead of a
+                # "xyz_type_n" synthetic name. Enum and model are the only types that would get their own
+                # Python class.
+                return process_items(items_with_classes[0])
+
+            return props, new_schemas
+
+        sub_properties, schemas = process_items()
+        if isinstance(sub_properties, PropertyError):
+            return sub_properties, schemas
 
         sub_properties, discriminators_from_nested_unions = _flatten_union_properties(sub_properties)
 

From b92fed6f52e64f321d79c64445b98cd23af2c0e7 Mon Sep 17 00:00:00 2001
From: Eli Bishop <eli.bishop@benchling.com>
Date: Fri, 15 Nov 2024 15:19:17 -0800
Subject: [PATCH 2/5] functional tests for union type fix

---
 .../test_enums_and_consts.py                  |  86 ++------
 .../generated_code_execution/test_unions.py   | 199 ++++++++++++++++++
 end_to_end_tests/functional_tests/helpers.py  |   3 +-
 end_to_end_tests/generated_client.py          |  10 +
 .../parser/properties/protocol.py             |   2 +-
 5 files changed, 224 insertions(+), 76 deletions(-)

diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py
index c612813a0..f04665f78 100644
--- a/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py
+++ b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py
@@ -133,88 +133,22 @@ def test_invalid_values(self, MyModel):
 """
 components:
   schemas:
-    MyEnum:
-      type: string
-      enum: ["a", "b"]
-    MyEnumIncludingNull:
-      type: ["string", "null"]
-      enum: ["a", "b", null]
-    MyNullOnlyEnum:
+    EnumOfNullOnly:
       enum: [null]
     MyModel:
       properties:
-        nullableEnumProp:
-          oneOf:
-            - {"$ref": "#/components/schemas/MyEnum"}
-            - type: "null"
-        enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
-        nullOnlyEnumProp: {"$ref": "#/components/schemas/MyNullOnlyEnum"}
+        nullOnlyEnumProp: {"$ref": "#/components/schemas/EnumOfNullOnly"}
+      required: ["nullOnlyEnumProp"]
 """)
 @with_generated_code_imports(
-    ".models.MyEnum",
-    ".models.MyEnumIncludingNull",
     ".models.MyModel",
-    ".types.Unset",
 )
-class TestNullableEnums:
-    def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNull):
-        assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
-        assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
-        assert_model_decode_encode(
-            MyModel,
-            {"enumIncludingNullProp": "a"},
-            MyModel(enum_including_null_prop=MyEnumIncludingNull.A),
-        )
-        assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None))
+class TestSingleValueNullEnum:
+    def test_enum_of_null_only(self, MyModel):
         assert_model_decode_encode(MyModel, {"nullOnlyEnumProp": None}, MyModel(null_only_enum_prop=None))
     
-    def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
-        assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
-        assert_model_property_type_hint(MyModel, "enum_including_null_prop", Union[MyEnumIncludingNull, None, Unset])
-        assert_model_property_type_hint(MyModel, "null_only_enum_prop", Union[None, Unset])
-
-
-@with_generated_client_fixture(
-"""
-openapi: 3.0.0
-
-components:
-  schemas:
-    MyEnum:
-      type: string
-      enum: ["a", "b"]
-    MyEnumIncludingNull:
-      type: string
-      nullable: true
-      enum: ["a", "b", null]
-    MyModel:
-      properties:
-        nullableEnumProp:
-          allOf:
-            - {"$ref": "#/components/schemas/MyEnum"}
-          nullable: true
-        enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
-""")
-@with_generated_code_imports(
-    ".models.MyEnum",
-    ".models.MyEnumIncludingNull",
-    ".models.MyModel",
-    ".types.Unset",
-)
-class TestNullableEnumsInOpenAPI30:
-    def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNull):
-        assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
-        assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
-        assert_model_decode_encode(
-            MyModel,
-            {"enumIncludingNullProp": "a"},
-            MyModel(enum_including_null_prop=MyEnumIncludingNull.A),
-        )
-        assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None))
-    
-    def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
-        assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
-        assert_model_property_type_hint(MyModel, "enum_including_null_prop", Union[MyEnumIncludingNull, None, Unset])
+    def test_type_hints(self, MyModel):
+        assert_model_property_type_hint(MyModel, "null_only_enum_prop", None)
 
 
 @with_generated_client_fixture(
@@ -259,6 +193,8 @@ def test_invalid_int(self, MyModel):
 
 @with_generated_client_fixture(
 """
+# Tests of literal_enums mode, where enums become a typing.Literal type instead of a class
+
 components:
   schemas:
     MyEnum:
@@ -303,6 +239,8 @@ def test_invalid_values(self, MyModel):
 
 @with_generated_client_fixture(
 """
+# Tests of literal_enums mode, where enums become a typing.Literal type instead of a class
+
 components:
   schemas:
     MyEnum:
@@ -347,6 +285,8 @@ def test_invalid_values(self, MyModel):
 
 @with_generated_client_fixture(
 """
+# Similar to some of the "union with null" tests in test_unions.py, but in literal_enums mode
+
 components:
   schemas:
     MyEnum:
diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py b/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py
index 3dbd8e2a6..be913c166 100644
--- a/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py
+++ b/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py
@@ -38,6 +38,8 @@ def test_type_hints(self, MyModel, Unset):
 
 @with_generated_client_fixture(
 """
+# Various use cases for oneOf
+
 components:
   schemas:
     ThingA:
@@ -152,6 +154,203 @@ def test_type_hints(self, ModelWithUnion, ModelWithRequiredUnion, ModelWithUnion
         )
 
 
+@with_generated_client_fixture(
+"""
+# Various use cases for a oneOf where one of the variants is null, since these are handled
+# a bit differently in the generator
+
+components:
+  schemas:
+    MyEnum:
+      type: string
+      enum: ["a", "b"]
+    MyObject:
+      type: object
+      properties:
+        name:
+          type: string
+    MyModel:
+      properties:
+        nullableEnumProp:
+          oneOf:
+            - {"$ref": "#/components/schemas/MyEnum"}
+            - type: "null"
+        nullableObjectProp:
+          oneOf:
+            - {"$ref": "#/components/schemas/MyObject"}
+            - type: "null"
+        inlineNullableObject:
+          # Note, the generated class for this should be called "MyModelInlineNullableObject",
+          # since the generator's rule for inline schemas that require their own class is to
+          # concatenate the property name to the parent schema name.
+          oneOf:
+            - type: object
+              properties:
+                name:
+                  type: string
+            - type: "null"
+""")
+@with_generated_code_imports(
+    ".models.MyEnum",
+    ".models.MyObject",
+    ".models.MyModel",
+    ".models.MyModelInlineNullableObject",
+    ".types.Unset",
+)
+class TestUnionsWithNull:
+    def test_nullable_enum_prop(self, MyModel, MyEnum):
+        assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
+        assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
+
+    def test_nullable_object_prop(self, MyModel, MyObject):
+        assert_model_decode_encode( MyModel, {"nullableObjectProp": None}, MyModel(nullable_object_prop=None))
+        assert_model_decode_encode( MyModel, {"nullableObjectProp": None}, MyModel(nullable_object_prop=None))
+
+    def test_nullable_object_prop_with_inline_schema(self, MyModel, MyModelInlineNullableObject):
+        assert_model_decode_encode(
+            MyModel,
+            {"inlineNullableObject": {"name": "a"}},
+            MyModel(inline_nullable_object=MyModelInlineNullableObject(name="a")),
+        )
+        assert_model_decode_encode( MyModel, {"inlineNullableObject": None}, MyModel(inline_nullable_object=None))
+    
+    def test_type_hints(self, MyModel, MyEnum, Unset):
+        assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
+        assert_model_property_type_hint(MyModel, "nullable_object_prop", Union[ForwardRef("MyObject"), None, Unset])
+        assert_model_property_type_hint(
+            MyModel,
+            "inline_nullable_object",
+            Union[ForwardRef("MyModelInlineNullableObject"), None, Unset],
+        )
+
+
+@with_generated_client_fixture(
+"""
+# Tests for combining the OpenAPI 3.0 "nullable" attribute with an enum
+
+openapi: 3.0.0
+
+components:
+  schemas:
+    MyEnum:
+      type: string
+      enum: ["a", "b"]
+    MyEnumIncludingNull:
+      type: string
+      nullable: true
+      enum: ["a", "b", null]
+    MyModel:
+      properties:
+        nullableEnumProp:
+          allOf:
+            - {"$ref": "#/components/schemas/MyEnum"}
+          nullable: true
+        enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
+""")
+@with_generated_code_imports(
+    ".models.MyEnum",
+    ".models.MyEnumIncludingNull",
+    ".models.MyModel",
+    ".types.Unset",
+)
+class TestNullableEnumsInOpenAPI30:
+    def test_nullable_enum_prop(self, MyModel, MyEnum, MyEnumIncludingNull):
+        assert_model_decode_encode(MyModel, {"nullableEnumProp": "b"}, MyModel(nullable_enum_prop=MyEnum.B))
+        assert_model_decode_encode(MyModel, {"nullableEnumProp": None}, MyModel(nullable_enum_prop=None))
+        assert_model_decode_encode(
+            MyModel,
+            {"enumIncludingNullProp": "a"},
+            MyModel(enum_including_null_prop=MyEnumIncludingNull.A),
+        )
+        assert_model_decode_encode( MyModel, {"enumIncludingNullProp": None}, MyModel(enum_including_null_prop=None))
+    
+    def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
+        assert_model_property_type_hint(MyModel, "nullable_enum_prop", Union[MyEnum, None, Unset])
+        assert_model_property_type_hint(MyModel, "enum_including_null_prop", Union[MyEnumIncludingNull, None, Unset])
+
+
+@with_generated_client_fixture(
+"""
+# Test use cases where there's a union of types *and* an explicit list of multiple "type:"s
+
+components:
+  schemas:
+    MyStringEnum:
+      type: string
+      enum: ["a", "b"]
+    MyIntEnum:
+      type: integer
+      enum: [1, 2]
+    MyEnumIncludingNull:
+      type: ["string", "null"]
+      enum: ["a", "b", null]
+    MyObject:
+      type: object
+      properties:
+        name:
+          type: string
+    MyModel:
+      properties:
+        enumsWithListOfTypesProp:
+          type: ["string", "integer"]
+          oneOf:
+            - {"$ref": "#/components/schemas/MyStringEnum"}
+            - {"$ref": "#/components/schemas/MyIntEnum"}
+        enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
+        nullableObjectWithListOfTypesProp:
+          type: ["string", "object"]
+          oneOf:
+            - {"$ref": "#/components/schemas/MyObject"}
+            - type: "null"
+""")
+@with_generated_code_imports(
+    ".models.MyStringEnum",
+    ".models.MyIntEnum",
+    ".models.MyEnumIncludingNull",
+    ".models.MyObject",
+    ".models.MyModel",
+    ".types.Unset",
+)
+class TestUnionsWithExplicitListOfTypes:
+    # This covers some use cases where combining "oneOf" with "type: [list of types]" (which is fine
+    # to do in OpenAPI) used to generate enum/model classes incorrectly.
+
+    def test_union_of_enums(self, MyModel, MyStringEnum, MyIntEnum):
+        assert_model_decode_encode(
+            MyModel,
+            {"enumsWithListOfTypesProp": "b"},
+            MyModel(enums_with_list_of_types_prop=MyStringEnum.B),
+        )
+        assert_model_decode_encode(
+            MyModel,
+            {"enumsWithListOfTypesProp": 2},
+            MyModel(enums_with_list_of_types_prop=MyIntEnum.VALUE_2),
+        )
+
+    def test_union_of_enum_with_null(self, MyModel, MyEnumIncludingNull):
+        assert_model_decode_encode(
+            MyModel,
+            {"enumIncludingNullProp": "b"},
+            MyModel(enum_including_null_prop=MyEnumIncludingNull.B),
+        )
+        assert_model_decode_encode(
+            MyModel,
+            {"enumIncludingNullProp": None},
+            MyModel(enum_including_null_prop=None),
+        )
+
+    def test_nullable_object_with_list_of_types(self, MyModel, MyObject):
+        assert_model_decode_encode(
+            MyModel,
+            {"nullableObjectWithListOfTypesProp": {"name": "a"}},
+            MyModel(nullable_object_with_list_of_types_prop=MyObject(name="a")),
+        )
+        assert_model_decode_encode(
+            MyModel,
+            {"nullableObjectWithListOfTypesProp": None},
+            MyModel(nullable_object_with_list_of_types_prop=None),
+        )
+    
 @with_generated_client_fixture(
 """
 components:
diff --git a/end_to_end_tests/functional_tests/helpers.py b/end_to_end_tests/functional_tests/helpers.py
index d7b090662..aadd5ede6 100644
--- a/end_to_end_tests/functional_tests/helpers.py
+++ b/end_to_end_tests/functional_tests/helpers.py
@@ -47,8 +47,7 @@ def _decorator(cls):
         nonlocal alias
 
         def _func(self, generated_client):
-            module = generated_client.import_module(module_name)
-            return getattr(module, import_name)
+            return generated_client.import_symbol(module_name, import_name)
         
         alias = alias or import_name
         _func.__name__ = alias
diff --git a/end_to_end_tests/generated_client.py b/end_to_end_tests/generated_client.py
index bb580604e..061dd41bc 100644
--- a/end_to_end_tests/generated_client.py
+++ b/end_to_end_tests/generated_client.py
@@ -45,6 +45,16 @@ def import_module(self, module_path: str) -> Any:
         """Attempt to import a module from the generated code."""
         return importlib.import_module(f"{self.base_module}{module_path}")
 
+    def import_symbol(self, module_path: str, name: str) -> Any:
+        module = self.import_module(module_path)
+        try:
+            return getattr(module, name)
+        except AttributeError:
+            existing = ", ".join(name for name in dir(module) if not name.startswith("_"))
+            assert False, (
+                f"Couldn't find import \"{name}\" in \"{self.base_module}{module_path}\"."
+                f" Available imports in that module are: {existing}"
+            )
 
 def _run_command(
     command: str,
diff --git a/openapi_python_client/parser/properties/protocol.py b/openapi_python_client/parser/properties/protocol.py
index 27f9dcd30..fa47f023e 100644
--- a/openapi_python_client/parser/properties/protocol.py
+++ b/openapi_python_client/parser/properties/protocol.py
@@ -200,4 +200,4 @@ class HasNamedClass(Protocol):
     class in the generated code. Currently that is ModelProperty and UnionProperty.
     """
 
-    class_info: Class
\ No newline at end of file
+    class_info: Class

From f4d3735271f7ba874c920c932b6b9d4928dc06c2 Mon Sep 17 00:00:00 2001
From: Eli Bishop <eli.bishop@benchling.com>
Date: Mon, 18 Nov 2024 11:01:18 -0800
Subject: [PATCH 3/5] misc fixes

---
 .../generated_code_execution/test_defaults.py |  32 +++
 .../generated_code_execution/test_unions.py   | 217 +++++++++---------
 end_to_end_tests/generated_client.py          |   8 +-
 .../parser/properties/union.py                |  36 ++-
 4 files changed, 171 insertions(+), 122 deletions(-)

diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py b/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py
index 5f8affb25..8f63df3fa 100644
--- a/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py
+++ b/end_to_end_tests/functional_tests/generated_code_execution/test_defaults.py
@@ -112,3 +112,35 @@ def test_enum_default(self, MyEnum, MyModel):
 class TestLiteralEnumDefaults:
     def test_default_value(self, MyModel):
         assert MyModel().enum_prop == "A"
+
+
+@with_generated_client_fixture(
+"""
+# Test the ability to specify a default value for a union type as long as that value is
+# supported by at least one of the variants
+
+components:
+  schemas:
+    MyModel:
+      type: object
+      properties:
+        simpleTypeProp1:
+          type: ["integer", "boolean", "string"]
+          default: 3
+        simpleTypeProp2:
+          type: ["integer", "boolean", "string"]
+          default: true
+        simpleTypeProp3:
+          type: ["integer", "boolean", "string"]
+          default: abc
+"""
+)
+@with_generated_code_imports(".models.MyModel")
+class TestUnionDefaults:
+    def test_simple_type(self, MyModel):
+        instance = MyModel()
+        assert instance == MyModel(
+            simple_type_prop_1=3,
+            simple_type_prop_2=True,
+            simple_type_prop_3="abc",
+        )
diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py b/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py
index be913c166..6326ae582 100644
--- a/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py
+++ b/end_to_end_tests/functional_tests/generated_code_execution/test_unions.py
@@ -10,32 +10,6 @@
 )
 
 
-@with_generated_client_fixture(
-"""
-components:
-  schemas:
-    StringOrInt:
-      type: ["string", "integer"]
-    MyModel:
-      type: object
-      properties:
-        stringOrIntProp:
-          type: ["string", "integer"]
-"""
-)
-@with_generated_code_imports(
-    ".models.MyModel",
-    ".types.Unset"
-)
-class TestSimpleTypeList:
-    def test_decode_encode(self, MyModel):
-        assert_model_decode_encode(MyModel, {"stringOrIntProp": "a"}, MyModel(string_or_int_prop="a"))
-        assert_model_decode_encode(MyModel, {"stringOrIntProp": 1}, MyModel(string_or_int_prop=1))
-
-    def test_type_hints(self, MyModel, Unset):
-        assert_model_property_type_hint(MyModel, "string_or_int_prop", Union[str, int, Unset])
-
-
 @with_generated_client_fixture(
 """
 # Various use cases for oneOf
@@ -271,88 +245,8 @@ def test_type_hints(self, MyModel, MyEnum, MyEnumIncludingNull, Unset):
 
 @with_generated_client_fixture(
 """
-# Test use cases where there's a union of types *and* an explicit list of multiple "type:"s
+# Tests for using a discriminator property
 
-components:
-  schemas:
-    MyStringEnum:
-      type: string
-      enum: ["a", "b"]
-    MyIntEnum:
-      type: integer
-      enum: [1, 2]
-    MyEnumIncludingNull:
-      type: ["string", "null"]
-      enum: ["a", "b", null]
-    MyObject:
-      type: object
-      properties:
-        name:
-          type: string
-    MyModel:
-      properties:
-        enumsWithListOfTypesProp:
-          type: ["string", "integer"]
-          oneOf:
-            - {"$ref": "#/components/schemas/MyStringEnum"}
-            - {"$ref": "#/components/schemas/MyIntEnum"}
-        enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
-        nullableObjectWithListOfTypesProp:
-          type: ["string", "object"]
-          oneOf:
-            - {"$ref": "#/components/schemas/MyObject"}
-            - type: "null"
-""")
-@with_generated_code_imports(
-    ".models.MyStringEnum",
-    ".models.MyIntEnum",
-    ".models.MyEnumIncludingNull",
-    ".models.MyObject",
-    ".models.MyModel",
-    ".types.Unset",
-)
-class TestUnionsWithExplicitListOfTypes:
-    # This covers some use cases where combining "oneOf" with "type: [list of types]" (which is fine
-    # to do in OpenAPI) used to generate enum/model classes incorrectly.
-
-    def test_union_of_enums(self, MyModel, MyStringEnum, MyIntEnum):
-        assert_model_decode_encode(
-            MyModel,
-            {"enumsWithListOfTypesProp": "b"},
-            MyModel(enums_with_list_of_types_prop=MyStringEnum.B),
-        )
-        assert_model_decode_encode(
-            MyModel,
-            {"enumsWithListOfTypesProp": 2},
-            MyModel(enums_with_list_of_types_prop=MyIntEnum.VALUE_2),
-        )
-
-    def test_union_of_enum_with_null(self, MyModel, MyEnumIncludingNull):
-        assert_model_decode_encode(
-            MyModel,
-            {"enumIncludingNullProp": "b"},
-            MyModel(enum_including_null_prop=MyEnumIncludingNull.B),
-        )
-        assert_model_decode_encode(
-            MyModel,
-            {"enumIncludingNullProp": None},
-            MyModel(enum_including_null_prop=None),
-        )
-
-    def test_nullable_object_with_list_of_types(self, MyModel, MyObject):
-        assert_model_decode_encode(
-            MyModel,
-            {"nullableObjectWithListOfTypesProp": {"name": "a"}},
-            MyModel(nullable_object_with_list_of_types_prop=MyObject(name="a")),
-        )
-        assert_model_decode_encode(
-            MyModel,
-            {"nullableObjectWithListOfTypesProp": None},
-            MyModel(nullable_object_with_list_of_types_prop=None),
-        )
-    
-@with_generated_client_fixture(
-"""
 components:
   schemas:
     ModelType1:
@@ -503,3 +397,112 @@ def test_nested_with_different_property(self, ModelType1, Schnauzer, WithNestedD
             {"unionProp": {"modelType": "irrelevant", "dogType": "Schnauzer", "name": "a"}},
             WithNestedDiscriminatorsDifferentProperty(union_prop=Schnauzer(model_type="irrelevant", dog_type="Schnauzer", name="a")),
         )
+
+
+@with_generated_client_fixture(
+"""
+# Tests for using multiple values of "type:" in one schema (OpenAPI 3.1)
+
+components:
+  schemas:
+    StringOrInt:
+      type: ["string", "integer"]
+    MyModel:
+      type: object
+      properties:
+        stringOrIntProp:
+          type: ["string", "integer"]
+"""
+)
+@with_generated_code_imports(
+    ".models.MyModel",
+    ".types.Unset"
+)
+class TestListOfSimpleTypes:
+    def test_decode_encode(self, MyModel):
+        assert_model_decode_encode(MyModel, {"stringOrIntProp": "a"}, MyModel(string_or_int_prop="a"))
+        assert_model_decode_encode(MyModel, {"stringOrIntProp": 1}, MyModel(string_or_int_prop=1))
+
+    def test_type_hints(self, MyModel, Unset):
+        assert_model_property_type_hint(MyModel, "string_or_int_prop", Union[str, int, Unset])
+
+
+@with_generated_client_fixture(
+"""
+# Test cases where there's a union of types *and* an explicit list of multiple "type:"s -
+# there was a bug where this could cause enum/model classes to be generated incorrectly
+
+components:
+  schemas:
+    MyStringEnum:
+      type: string
+      enum: ["a", "b"]
+    MyIntEnum:
+      type: integer
+      enum: [1, 2]
+    MyEnumIncludingNull:
+      type: ["string", "null"]
+      enum: ["a", "b", null]
+    MyObject:
+      type: object
+      properties:
+        name:
+          type: string
+    MyModel:
+      properties:
+        enumsWithListOfTypesProp:
+          type: ["string", "integer"]
+          oneOf:
+            - {"$ref": "#/components/schemas/MyStringEnum"}
+            - {"$ref": "#/components/schemas/MyIntEnum"}
+        enumIncludingNullProp: {"$ref": "#/components/schemas/MyEnumIncludingNull"}
+        nullableObjectWithListOfTypesProp:
+          type: ["string", "object"]
+          oneOf:
+            - {"$ref": "#/components/schemas/MyObject"}
+            - type: "null"
+""")
+@with_generated_code_imports(
+    ".models.MyStringEnum",
+    ".models.MyIntEnum",
+    ".models.MyEnumIncludingNull",
+    ".models.MyObject",
+    ".models.MyModel",
+    ".types.Unset",
+)
+class TestUnionsWithListOfSimpleTypes:
+    def test_union_of_enums(self, MyModel, MyStringEnum, MyIntEnum):
+        assert_model_decode_encode(
+            MyModel,
+            {"enumsWithListOfTypesProp": "b"},
+            MyModel(enums_with_list_of_types_prop=MyStringEnum.B),
+        )
+        assert_model_decode_encode(
+            MyModel,
+            {"enumsWithListOfTypesProp": 2},
+            MyModel(enums_with_list_of_types_prop=MyIntEnum.VALUE_2),
+        )
+
+    def test_union_of_enum_with_null(self, MyModel, MyEnumIncludingNull):
+        assert_model_decode_encode(
+            MyModel,
+            {"enumIncludingNullProp": "b"},
+            MyModel(enum_including_null_prop=MyEnumIncludingNull.B),
+        )
+        assert_model_decode_encode(
+            MyModel,
+            {"enumIncludingNullProp": None},
+            MyModel(enum_including_null_prop=None),
+        )
+
+    def test_nullable_object_with_list_of_types(self, MyModel, MyObject):
+        assert_model_decode_encode(
+            MyModel,
+            {"nullableObjectWithListOfTypesProp": {"name": "a"}},
+            MyModel(nullable_object_with_list_of_types_prop=MyObject(name="a")),
+        )
+        assert_model_decode_encode(
+            MyModel,
+            {"nullableObjectWithListOfTypesProp": None},
+            MyModel(nullable_object_with_list_of_types_prop=None),
+        )
diff --git a/end_to_end_tests/generated_client.py b/end_to_end_tests/generated_client.py
index 061dd41bc..8007e6384 100644
--- a/end_to_end_tests/generated_client.py
+++ b/end_to_end_tests/generated_client.py
@@ -52,8 +52,9 @@ def import_symbol(self, module_path: str, name: str) -> Any:
         except AttributeError:
             existing = ", ".join(name for name in dir(module) if not name.startswith("_"))
             assert False, (
-                f"Couldn't find import \"{name}\" in \"{self.base_module}{module_path}\"."
-                f" Available imports in that module are: {existing}"
+                f"Couldn't find import \"{name}\" in \"{self.base_module}{module_path}\".\n"
+                f"Available imports in that module are: {existing}\n"
+                f"Output from generator was: {self.generator_result.stdout}"
             )
 
 def _run_command(
@@ -77,7 +78,8 @@ def _run_command(
         args.extend(extra_args)
     result = runner.invoke(app, args)
     if result.exit_code != 0 and raise_on_error:
-        raise Exception(result.stdout)
+        message = f"{result.stdout}\n{result.exception}" if result.exception else result.stdout
+        raise Exception(message)
     return result
 
 
diff --git a/openapi_python_client/parser/properties/union.py b/openapi_python_client/parser/properties/union.py
index 574b44c7f..0d30fd3d6 100644
--- a/openapi_python_client/parser/properties/union.py
+++ b/openapi_python_client/parser/properties/union.py
@@ -77,21 +77,29 @@ def build(
         """
         from . import property_from_data
 
-        sub_properties: list[PropertyProtocol] = []
-
         type_list_data = []
         if isinstance(data.type, list) and not (data.anyOf or data.oneOf):
+            # The schema specifies "type:" with a list of allowable types. If there is *not* also an "anyOf"
+            # or "oneOf", then we should treat that as a shorthand for a oneOf where each variant is just
+            # a single "type:". For example:
+            #   {"type": ["string", "int"]} becomes
+            #   {"oneOf": [{"type": "string"}, {"type": "int"}]}
+            # However, if there *is* also an "anyOf" or "oneOf" list, then the information from "type:" is
+            # redundant since every allowable variant type is already fully described in the list.
             for _type in data.type:
                 type_list_data.append(data.model_copy(update={"type": _type, "default": None}))
+                # Here we're copying properties from the top-level union schema that might apply to one
+                # of the type variants, like "format" for a string. But we don't copy "default" because
+                # default values will be handled at the top level by the UnionProperty.
 
         def process_items(
-            preserve_name_for_item: oai.Schema | oai.Reference | None = None,
+            use_original_name_for: oai.Schema | oai.Reference | None = None,
         ) -> tuple[list[PropertyProtocol] | PropertyError, Schemas]:
             props: list[PropertyProtocol] = []
             new_schemas = schemas
-            items_with_classes: list[oai.Schema | oai.Reference] = []
+            schemas_with_classes: list[oai.Schema | oai.Reference] = []
             for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf, type_list_data)):
-                sub_prop_name = name if sub_prop_data is preserve_name_for_item else f"{name}_type_{i}"
+                sub_prop_name = name if sub_prop_data is use_original_name_for else f"{name}_type_{i}"
                 sub_prop, new_schemas = property_from_data(
                     name=sub_prop_name,
                     required=True,
@@ -103,15 +111,19 @@ def process_items(
                 if isinstance(sub_prop, PropertyError):
                     return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), new_schemas
                 if isinstance(sub_prop, HasNamedClass):
-                    items_with_classes.append(sub_prop_data)
+                    schemas_with_classes.append(sub_prop_data)
                 props.append(sub_prop)
 
-            if (not preserve_name_for_item) and (len(items_with_classes) == 1):
-                # After our first pass, if it turns out that there was exactly one enum or model in the list,
-                # then we'll do a second pass where we use the original name for that item instead of a
-                # "xyz_type_n" synthetic name. Enum and model are the only types that would get their own
-                # Python class.
-                return process_items(items_with_classes[0])
+            if (not use_original_name_for) and len(schemas_with_classes) == 1:
+                # An example of this scenario is a oneOf where one of the variants is an inline enum or
+                # model, and the other is a simple value like null. If the name of the union property is
+                # "foo" then it's desirable for the enum or model class to be named "Foo", not "FooType1".
+                # So, we'll do a second pass where we tell ourselves to use the original property name
+                # for that item instead of "{name}_type_{i}".
+                # This only makes a functional difference if the variant was an inline schema, because
+                # we wouldn't be generating a class otherwise, but even if it wasn't inline this will
+                # save on pointlessly long variable names inside from_dict/to_dict.
+                return process_items(use_original_name_for=schemas_with_classes[0])
 
             return props, new_schemas
 

From 668ea043a7c145baaa6229f1134bfc44bde8dc13 Mon Sep 17 00:00:00 2001
From: Eli Bishop <eli.bishop@benchling.com>
Date: Fri, 6 Dec 2024 10:33:44 -0800
Subject: [PATCH 4/5] clarify nullable special-case logic

---
 .../parser/properties/union.py                | 62 ++++++++++++++-----
 1 file changed, 47 insertions(+), 15 deletions(-)

diff --git a/openapi_python_client/parser/properties/union.py b/openapi_python_client/parser/properties/union.py
index 0d30fd3d6..9ba112ac4 100644
--- a/openapi_python_client/parser/properties/union.py
+++ b/openapi_python_client/parser/properties/union.py
@@ -1,10 +1,12 @@
 from __future__ import annotations
 
 from itertools import chain
-from typing import Any, ClassVar, Mapping, OrderedDict, cast
+from typing import Any, Callable, ClassVar, Mapping, OrderedDict, cast
 
 from attr import define, evolve
 
+from openapi_python_client.parser.properties.none import NoneProperty
+
 from ... import Config
 from ... import schema as oai
 from ...utils import PythonIdentifier
@@ -92,14 +94,47 @@ def build(
                 # of the type variants, like "format" for a string. But we don't copy "default" because
                 # default values will be handled at the top level by the UnionProperty.
 
+        def _add_index_suffix_to_variant_names(index: int) -> str:
+            return f"{name}_type_{index}"
+
+        def _use_same_name_as_parent(index: int) -> str:
+            return name
+
         def process_items(
-            use_original_name_for: oai.Schema | oai.Reference | None = None,
+            variant_name_from_index_func: Callable[[int], str],
         ) -> tuple[list[PropertyProtocol] | PropertyError, Schemas]:
             props: list[PropertyProtocol] = []
             new_schemas = schemas
             schemas_with_classes: list[oai.Schema | oai.Reference] = []
             for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf, type_list_data)):
-                sub_prop_name = name if sub_prop_data is use_original_name_for else f"{name}_type_{i}"
+                sub_prop_name = variant_name_from_index_func(i)
+
+                # The sub_prop_name logic is what makes this a bit complicated. That value is used only
+                # if sub_prop is an *inline* schema and needs us to make up a name for it. For instance,
+                # in the following schema--
+                #
+                #   MyModel:
+                #     properties:
+                #       unionThing:
+                #         oneOf:
+                #           - type: object
+                #             properties: ...
+                #           - type: object
+                #             properties: ...
+                #
+                # --both of the variants under oneOf are inline schemas. And since they're objects, we
+                # will be creating model classes for them, which need names. Inline schemas are named by
+                # concatenating names of parents; so, when we're in UnionProperty.build() for unionThing,
+                # the value of "name" is "my_model_union_thing", and then we set sub_prop_name to
+                # "my_model_union_thing_type_0" and "my_model_union_thing_type_1" for the two variants,
+                # and their model classes will be MyModelUnionThingType0 and MyModelUnionThingType1.
+                #
+                # However, in this example, if the second variant was just "type: null" instead of an
+                # object (i.e. if unionThing is a nullable object value)... then it would be friendlier
+                # to call the first variant's class just MyModelUnionThing, not MyModelUnionThingType0.
+                # We'll check for that special case below; we can't know if that's the situation until
+                # after we've processed all the variants.
+
                 sub_prop, new_schemas = property_from_data(
                     name=sub_prop_name,
                     required=True,
@@ -114,22 +149,19 @@ def process_items(
                     schemas_with_classes.append(sub_prop_data)
                 props.append(sub_prop)
 
-            if (not use_original_name_for) and len(schemas_with_classes) == 1:
-                # An example of this scenario is a oneOf where one of the variants is an inline enum or
-                # model, and the other is a simple value like null. If the name of the union property is
-                # "foo" then it's desirable for the enum or model class to be named "Foo", not "FooType1".
-                # So, we'll do a second pass where we tell ourselves to use the original property name
-                # for that item instead of "{name}_type_{i}".
-                # This only makes a functional difference if the variant was an inline schema, because
-                # we wouldn't be generating a class otherwise, but even if it wasn't inline this will
-                # save on pointlessly long variable names inside from_dict/to_dict.
-                return process_items(use_original_name_for=schemas_with_classes[0])
-
             return props, new_schemas
 
-        sub_properties, schemas = process_items()
+        sub_properties, schemas = process_items(_add_index_suffix_to_variant_names)
         if isinstance(sub_properties, PropertyError):
             return sub_properties, schemas
+        
+        # Here's the check for the special case described above. If the variants are just "inline
+        # object or enum" and "null", we'll re-process them to adjust the naming.
+        if (len([p for p in sub_properties if isinstance(p, HasNamedClass)]) == 1 and
+            len([p for p in sub_properties if isinstance(p, NoneProperty)]) == 1):
+            sub_properties, schemas = process_items(_use_same_name_as_parent)
+            if isinstance(sub_properties, PropertyError):
+                return sub_properties, schemas
 
         sub_properties, discriminators_from_nested_unions = _flatten_union_properties(sub_properties)
 

From a5ed889b975bde48326f9ec90ad9912f0ad7ee5f Mon Sep 17 00:00:00 2001
From: Eli Bishop <eli.bishop@benchling.com>
Date: Fri, 6 Dec 2024 11:03:12 -0800
Subject: [PATCH 5/5] further clarification

---
 .../parser/properties/union.py                | 45 ++++++++++---------
 1 file changed, 24 insertions(+), 21 deletions(-)

diff --git a/openapi_python_client/parser/properties/union.py b/openapi_python_client/parser/properties/union.py
index 9ba112ac4..c1dc9120d 100644
--- a/openapi_python_client/parser/properties/union.py
+++ b/openapi_python_client/parser/properties/union.py
@@ -97,15 +97,11 @@ def build(
         def _add_index_suffix_to_variant_names(index: int) -> str:
             return f"{name}_type_{index}"
 
-        def _use_same_name_as_parent(index: int) -> str:
-            return name
-
         def process_items(
-            variant_name_from_index_func: Callable[[int], str],
+            variant_name_from_index_func: Callable[[int], str] = _add_index_suffix_to_variant_names,
         ) -> tuple[list[PropertyProtocol] | PropertyError, Schemas]:
             props: list[PropertyProtocol] = []
             new_schemas = schemas
-            schemas_with_classes: list[oai.Schema | oai.Reference] = []
             for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf, type_list_data)):
                 sub_prop_name = variant_name_from_index_func(i)
 
@@ -129,11 +125,11 @@ def process_items(
                 # "my_model_union_thing_type_0" and "my_model_union_thing_type_1" for the two variants,
                 # and their model classes will be MyModelUnionThingType0 and MyModelUnionThingType1.
                 #
-                # However, in this example, if the second variant was just "type: null" instead of an
-                # object (i.e. if unionThing is a nullable object value)... then it would be friendlier
-                # to call the first variant's class just MyModelUnionThing, not MyModelUnionThingType0.
-                # We'll check for that special case below; we can't know if that's the situation until
-                # after we've processed all the variants.
+                # However, in this example, if the second variant was just a scalar type instead of an
+                # object (like "type: null" or "type: string"), so that the first variant is the only
+                # one that needs a class... then it would be friendlier to call the first variant's
+                # class just MyModelUnionThing, not MyModelUnionThingType0. We'll check for that special
+                # case below; we can't know if that's the situation until after we've processed them all.
 
                 sub_prop, new_schemas = property_from_data(
                     name=sub_prop_name,
@@ -145,23 +141,30 @@ def process_items(
                 )
                 if isinstance(sub_prop, PropertyError):
                     return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), new_schemas
-                if isinstance(sub_prop, HasNamedClass):
-                    schemas_with_classes.append(sub_prop_data)
                 props.append(sub_prop)
 
             return props, new_schemas
 
-        sub_properties, schemas = process_items(_add_index_suffix_to_variant_names)
+        sub_properties, new_schemas = process_items()
+        # Here's the check for the special case described above. If just one of the variants is
+        # an inline schema whose name matters, then we'll re-process them to simplify the naming.
+        # Unfortunately we do have to re-process them all; we can't just modify that one variant
+        # in place, because new_schemas already contains several references to its old name.
+        if (
+            not isinstance(sub_properties, PropertyError)
+            and len([p for p in sub_properties if isinstance(p, HasNamedClass)]) == 1
+        ):
+            def _use_same_name_as_parent_for_that_one_variant(index: int) -> str:
+                for i, p in enumerate(sub_properties):
+                    if i == index and isinstance(p, HasNamedClass):
+                        return name
+                return _add_index_suffix_to_variant_names(index)
+
+            sub_properties, new_schemas = process_items(_use_same_name_as_parent_for_that_one_variant)
+
         if isinstance(sub_properties, PropertyError):
             return sub_properties, schemas
-        
-        # Here's the check for the special case described above. If the variants are just "inline
-        # object or enum" and "null", we'll re-process them to adjust the naming.
-        if (len([p for p in sub_properties if isinstance(p, HasNamedClass)]) == 1 and
-            len([p for p in sub_properties if isinstance(p, NoneProperty)]) == 1):
-            sub_properties, schemas = process_items(_use_same_name_as_parent)
-            if isinstance(sub_properties, PropertyError):
-                return sub_properties, schemas
+        schemas = new_schemas
 
         sub_properties, discriminators_from_nested_unions = _flatten_union_properties(sub_properties)