8181from datamodel_code_generator .parser .generation import GenerationIndex , GenerationStore , set_model_base_classes
8282from datamodel_code_generator .parser .schema_version import SchemaFeaturesT
8383from datamodel_code_generator .reference import ModelResolver , ModelType , Reference
84- from datamodel_code_generator .types import ANY , DataType , DataTypeManager
84+ from datamodel_code_generator .types import ANY , NONE , DataType , DataTypeManager
8585from datamodel_code_generator .util import camel_to_snake
8686
8787if TYPE_CHECKING :
@@ -1100,6 +1100,145 @@ def _get_discriminator_field_value(discriminator_field: DataModelFieldBase) -> D
11001100 return None
11011101
11021102
1103+ def _find_discriminator_value (fields : Iterable [DataModelFieldBase ], field_name : str ) -> DiscriminatorValue | None :
1104+ for field in fields :
1105+ if (
1106+ field_name in {field .original_name , field .name }
1107+ and (value := _get_discriminator_field_value (field )) is not None
1108+ ):
1109+ return value
1110+ return None
1111+
1112+
1113+ def _get_discriminator_values (
1114+ discriminator_model : DataModel ,
1115+ field_name : str ,
1116+ mapping : dict [str , str ],
1117+ * ,
1118+ require_literal : bool = False ,
1119+ ) -> list [DiscriminatorValue ]:
1120+ if (value := _find_discriminator_value (discriminator_model .fields , field_name )) is not None :
1121+ return [value ]
1122+
1123+ # Reuse models are created as empty subclasses with a "/reuse" path suffix.
1124+ # Nested choices cannot be updated later, so also accept inherited literals.
1125+ if (require_literal or discriminator_model .path .endswith ("/reuse" )) and (
1126+ value := _find_discriminator_value (discriminator_model .iter_all_fields (), field_name )
1127+ ) is not None :
1128+ return [value ]
1129+ if require_literal :
1130+ return []
1131+
1132+ discriminator_values : list [DiscriminatorValue ] = []
1133+ if mapping :
1134+ _check_discriminator_mapping_paths (discriminator_model , mapping , discriminator_values )
1135+ if not discriminator_values :
1136+ for base_class in discriminator_model .base_classes :
1137+ if base_class .reference :
1138+ _check_discriminator_mapping_paths (base_class .reference , mapping , discriminator_values )
1139+
1140+ return discriminator_values or [discriminator_model .path .split ("/" )[- 1 ]]
1141+
1142+
1143+ def _remove_discriminator (field : DataModelFieldBase ) -> None :
1144+ field .extras .pop ("discriminator" , None )
1145+ field .data_type .discriminator = None
1146+
1147+
1148+ def _is_discriminator_container (data_type : DataType ) -> bool :
1149+ return (
1150+ data_type .is_dict
1151+ or data_type .is_list
1152+ or data_type .is_set
1153+ or data_type .is_frozen_set
1154+ or data_type .is_mapping
1155+ or data_type .is_sequence
1156+ or data_type .is_tuple
1157+ )
1158+
1159+
1160+ def _iter_discriminator_data_types (
1161+ data_types : Iterable [DataType ],
1162+ pydantic_v2_root_model_type : type [DataModel ] | None ,
1163+ active_union_models : set [int ] | None = None ,
1164+ * ,
1165+ can_update_discriminator : bool = True ,
1166+ discriminator_owner : int | None = None ,
1167+ ) -> Iterator [tuple [DataType , bool , int ]]:
1168+ for data_type in data_types :
1169+ if data_type .is_union and not _is_discriminator_container (data_type ):
1170+ yield from _iter_discriminator_data_types (
1171+ data_type .data_types ,
1172+ pydantic_v2_root_model_type ,
1173+ active_union_models ,
1174+ can_update_discriminator = False ,
1175+ discriminator_owner = discriminator_owner ,
1176+ )
1177+ else :
1178+ source = data_type .reference .source if data_type .reference else None
1179+ owner = discriminator_owner or (id (source ) if source is not None else id (data_type ))
1180+ if not isinstance (source , DataModel ) or not (
1181+ isinstance (source , TypeAliasBase ) or _is_pydantic_v2_root_model (source , pydantic_v2_root_model_type )
1182+ ):
1183+ yield data_type , can_update_discriminator , owner
1184+ else :
1185+ source_id = id (source )
1186+ if active_union_models is None :
1187+ active_union_models = set ()
1188+ if source_id in active_union_models or not source .fields :
1189+ yield data_type , can_update_discriminator , owner
1190+ else :
1191+ active_union_models .add (source_id )
1192+ try :
1193+ yield from _iter_discriminator_data_types (
1194+ (source .fields [0 ].data_type ,),
1195+ pydantic_v2_root_model_type ,
1196+ active_union_models ,
1197+ can_update_discriminator = False ,
1198+ discriminator_owner = owner ,
1199+ )
1200+ finally :
1201+ active_union_models .remove (source_id )
1202+
1203+
1204+ def _discriminator_variants_are_valid (
1205+ data_types : Iterable [DataType ],
1206+ field_name : str ,
1207+ mapping : dict [str , str ],
1208+ pydantic_v2_root_model_type : type [DataModel ] | None ,
1209+ ) -> bool :
1210+ discriminator_value_owners : dict [DiscriminatorValue , int ] = {}
1211+ for data_type , can_update_discriminator , owner in _iter_discriminator_data_types (
1212+ data_types , pydantic_v2_root_model_type
1213+ ):
1214+ if not data_type .reference and data_type .type == NONE :
1215+ continue
1216+ if _is_discriminator_container (data_type ) or not data_type .reference :
1217+ return False
1218+ discriminator_model = data_type .reference .source
1219+ if (
1220+ not isinstance (discriminator_model , DataModel )
1221+ or not discriminator_model .SUPPORTS_DISCRIMINATOR
1222+ or isinstance (discriminator_model , TypeAliasBase )
1223+ or _is_pydantic_v2_root_model (discriminator_model , pydantic_v2_root_model_type )
1224+ ):
1225+ return False
1226+
1227+ discriminator_values = _get_discriminator_values (
1228+ discriminator_model ,
1229+ field_name ,
1230+ mapping ,
1231+ require_literal = not can_update_discriminator ,
1232+ )
1233+ if not discriminator_values :
1234+ return False
1235+ for value in discriminator_values :
1236+ if (previous_owner := discriminator_value_owners .get (value )) is not None and previous_owner != owner :
1237+ return False
1238+ discriminator_value_owners [value ] = owner
1239+ return True
1240+
1241+
11031242def _get_enum_from_base (discriminator_model : DataModel , field_name : str ) -> Enum | None :
11041243 for base_class in discriminator_model .base_classes :
11051244 if not base_class .reference or not base_class .reference .source : # pragma: no cover
@@ -2051,12 +2190,11 @@ def __apply_discriminator_type( # noqa: PLR0912, PLR0914, PLR0915
20512190 ) -> None :
20522191 for model in models : # noqa: PLR1702
20532192 for field in model .fields :
2054- discriminator = field .extras .get ("discriminator" )
2055- if not discriminator or not isinstance (discriminator , dict ):
2056- continue
2057- property_name = discriminator .get ("propertyName" )
2058- if not property_name : # pragma: no cover
2059- continue
2193+ match field .extras .get ("discriminator" ):
2194+ case {"propertyName" : str () as property_name } as discriminator if property_name :
2195+ pass
2196+ case _:
2197+ continue
20602198 field_name , alias = self .model_resolver .get_valid_field_name_and_alias (
20612199 field_name = property_name , model_type = self .field_name_model_type
20622200 )
@@ -2065,64 +2203,32 @@ def __apply_discriminator_type( # noqa: PLR0912, PLR0914, PLR0915
20652203 # Any type cannot be a discriminated union variant (Pydantic v2 rejects it)
20662204 has_any_variant = any (_is_any_variant (dt ) for dt in field .data_type .data_types )
20672205 if has_any_variant : # pragma: no cover
2068- field .extras .pop ("discriminator" , None )
2069- field .data_type .discriminator = None
2206+ _remove_discriminator (field )
20702207 _clear_model_imports_cache_if_retained (model , can_retain_cache = can_retain_cache )
20712208 continue
2209+ if not _discriminator_variants_are_valid (
2210+ field .data_type .data_types ,
2211+ field_name ,
2212+ mapping ,
2213+ self .pydantic_v2_root_model_type ,
2214+ ):
2215+ _remove_discriminator (field )
2216+ _clear_model_imports_cache_if_retained (model , can_retain_cache = can_retain_cache )
2217+ continue
2218+
20722219 for data_type in field .data_type .data_types :
20732220 if not data_type .reference : # pragma: no cover
20742221 continue
20752222 discriminator_model = data_type .reference .source
2076-
20772223 if (
2078- not isinstance (discriminator_model , DataModel ) or not discriminator_model .SUPPORTS_DISCRIMINATOR
2224+ not isinstance (discriminator_model , DataModel )
2225+ or not discriminator_model .SUPPORTS_DISCRIMINATOR
2226+ or isinstance (discriminator_model , TypeAliasBase )
2227+ or _is_pydantic_v2_root_model (discriminator_model , self .pydantic_v2_root_model_type )
20792228 ): # pragma: no cover
20802229 continue
20812230
2082- discriminator_values : list [DiscriminatorValue ] = []
2083-
2084- for discriminator_field in discriminator_model .fields :
2085- if field_name not in {discriminator_field .original_name , discriminator_field .name }:
2086- continue
2087- discriminator_value = _get_discriminator_field_value (discriminator_field )
2088- if discriminator_value is not None :
2089- discriminator_values = [discriminator_value ]
2090- break
2091- # Reuse models are created as empty subclasses with a "/reuse" path suffix.
2092- # Scan inherited fields to recover the discriminator literal from the base.
2093- if not discriminator_values and discriminator_model .path .endswith ("/reuse" ):
2094- for discriminator_field in discriminator_model .iter_all_fields (): # pragma: no branch
2095- if field_name not in {discriminator_field .original_name , discriminator_field .name }:
2096- continue
2097- discriminator_value = _get_discriminator_field_value (discriminator_field )
2098- if discriminator_value is not None : # pragma: no branch
2099- discriminator_values = [discriminator_value ]
2100- break
2101-
2102- if not discriminator_values and mapping :
2103- _check_discriminator_mapping_paths (
2104- discriminator_model ,
2105- mapping ,
2106- discriminator_values ,
2107- )
2108-
2109- if len (discriminator_values ) == 0 :
2110- for base_class in discriminator_model .base_classes :
2111- if not base_class .reference :
2112- continue
2113-
2114- _check_discriminator_mapping_paths (base_class .reference , mapping , discriminator_values )
2115-
2116- if not discriminator_values :
2117- discriminator_values = [discriminator_model .path .split ("/" )[- 1 ]]
2118-
2119- if not discriminator_values :
2120- discriminator_values = [discriminator_model .path .split ("/" )[- 1 ]]
2121-
2122- if not discriminator_values : # pragma: no cover
2123- msg = f"Discriminator type is not found. { data_type .reference .path } "
2124- raise RuntimeError (msg )
2125-
2231+ discriminator_values = _get_discriminator_values (discriminator_model , field_name , mapping )
21262232 has_one_literal = False
21272233 for discriminator_field in discriminator_model .fields :
21282234 if field_name not in {discriminator_field .original_name , discriminator_field .name }:
@@ -2565,11 +2671,18 @@ def __collapse_root_models( # noqa: PLR0912, PLR0914, PLR0915
25652671 if isinstance (discriminator , dict )
25662672 else discriminator
25672673 )
2674+ mapping = discriminator .get ("mapping" , {}) if isinstance (discriminator , dict ) else {}
25682675 field_name , _ = self .model_resolver .get_valid_field_name_and_alias (
25692676 field_name = prop_name ,
25702677 model_type = self .field_name_model_type ,
25712678 )
2572- copied_data_type .discriminator = field_name
2679+ if _discriminator_variants_are_valid (
2680+ copied_data_type .data_types ,
2681+ field_name ,
2682+ mapping ,
2683+ self .pydantic_v2_root_model_type ,
2684+ ):
2685+ copied_data_type .discriminator = field_name
25732686 assert isinstance (data_type .parent , DataType )
25742687 self .generation_store .replace_nested_data_type (data_type .parent , data_type , copied_data_type )
25752688
0 commit comments