@@ -78,6 +78,10 @@ public class OpenAPINormalizer {
7878 // are removed as most generators cannot handle such case at the moment
7979 final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY" ;
8080
81+ // when set to true, oneOf is removed and is converted into mappings in a discriminator mapping
82+ final String REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING = "REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING" ;
83+
84+
8185 // when set to true, oneOf/anyOf with either string or enum string as sub schemas will be simplified
8286 // to just string
8387 final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING" ;
@@ -214,6 +218,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
214218 ruleNames .add (SIMPLIFY_ONEOF_ANYOF_ENUM );
215219 ruleNames .add (REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT );
216220 ruleNames .add (SORT_MODEL_PROPERTIES );
221+ ruleNames .add (REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING );
217222
218223 // rules that are default to true
219224 rules .put (SIMPLIFY_ONEOF_ANYOF , true );
@@ -639,6 +644,10 @@ protected void normalizeComponentsSchemas() {
639644
640645 // normalize the schemas
641646 schemas .put (schemaName , normalizeSchema (schema , new HashSet <>()));
647+
648+ if (getRule (REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING )) {
649+ ensureInheritanceForDiscriminatorMappings (schema , schemaName );
650+ }
642651 }
643652 }
644653 }
@@ -1069,6 +1078,7 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
10691078 // update sub-schema with the updated schema
10701079 schema .getOneOf ().set (i , normalizeSchema ((Schema ) item , visitedSchemas ));
10711080 }
1081+ schema = processReplaceOneOfByMapping (schema );
10721082 } else {
10731083 // normalize it as it's no longer an oneOf
10741084 schema = normalizeSchema (schema , visitedSchemas );
@@ -1564,6 +1574,245 @@ protected Schema processSimplifyOneOf(Schema schema) {
15641574 return schema ;
15651575 }
15661576
1577+
1578+ /**
1579+ * Ensure inheritance is correctly defined for OneOf and Discriminators.
1580+ *
1581+ * For schemas containing oneOf and discriminator.propertyName:
1582+ * <ul>
1583+ * <li>Create the mappings as $refs</li>
1584+ * <li>Remove OneOf</li>
1585+ * </ul>
1586+ */
1587+ protected Schema processReplaceOneOfByMapping (Schema schema ) {
1588+ if (!getRule (REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING ) || schema .getOneOf () == null ) {
1589+ return schema ;
1590+ }
1591+ Discriminator discriminator = schema .getDiscriminator ();
1592+ if (discriminator != null ) {
1593+ boolean inlineSchema = isInlineSchema (schema );
1594+ if (inlineSchema ) {
1595+ // the For referenced schemas, ensure that there is an allOf with this schema.
1596+ LOGGER .warn ("Inline oneOf schema not supported by REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization" );
1597+ return schema ;
1598+ }
1599+ if (discriminator .getMapping () == null && discriminator .getPropertyName () != null ) {
1600+ List <Schema > oneOfs = schema .getOneOf ();
1601+ if (oneOfs .stream ().anyMatch (oneOf -> oneOf .get$ref () == null )) {
1602+ LOGGER .warn ("oneOf should only contain $ref for REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization" );
1603+ return schema ;
1604+ }
1605+ Map <String , String > mappings = new TreeMap <>();
1606+ // is the discriminator qttribute qlready in this schema?
1607+ // if yes, it will be deleted in references oneOf to avoid duplicates
1608+ boolean hasProperty = findProperty (schema , discriminator .getPropertyName (), false , new HashSet <>()) != null ;
1609+ discriminator .setMapping (mappings );
1610+ for (Schema oneOf : oneOfs ) {
1611+ String refSchema = oneOf .get$ref ();
1612+ String name = getDiscriminatorValue (refSchema , discriminator .getPropertyName (), hasProperty , new HashSet <>(List .of (schema )));
1613+ mappings .put (name , refSchema );
1614+
1615+ }
1616+ // remove oneOf and only keep the new discriminator mapping
1617+ schema .oneOf (null );
1618+ } else if (discriminator .getPropertyName () == null ) {
1619+ LOGGER .warn ("Missing property name in discriminator" );
1620+ } else if (discriminator .getMapping () != null && discriminator .getMapping ().size () != schema .getOneOf ().size ()) {
1621+ LOGGER .warn ("Discriminator mapping size " + discriminator .getMapping ().size () + " mismatch with oneOf size " + schema .getOneOf ().size ());
1622+ } else {
1623+ // remove oneOf and only keep the discriminator mapping
1624+ LOGGER .info ("Removing oneOf, discriminator mapping takes precedences on OneOfs" );
1625+ schema .oneOf (null );
1626+ }
1627+ }
1628+
1629+ return schema ;
1630+ }
1631+
1632+ private boolean isInlineSchema (Schema schema ) {
1633+ if (openAPI .getComponents ()!=null && openAPI .getComponents ().getSchemas ()!=null ) {
1634+ int identity = System .identityHashCode (schema );
1635+ for (Schema componentSchema : openAPI .getComponents ().getSchemas ().values ()) {
1636+ if (System .identityHashCode (componentSchema ) == identity ) {
1637+ return false ;
1638+ }
1639+ }
1640+ }
1641+ return true ;
1642+ }
1643+
1644+ /**
1645+ * Best effort to retrieve a good discriminator value.
1646+ * By order of precedence:
1647+ * <ul>
1648+ * <li>x-discriminator-value</li>
1649+ * <li>single enum value for attribute used by the discriminator.propertyName</li>
1650+ * <li>hame of the schema</li>
1651+ * </ul>
1652+ *
1653+ * @param refSchema $ref value like #/components/schemas/Dog
1654+ * @param discriminatorPropertyName name of the property used in the discriminator mapping
1655+ * @param propertyAlreadyPresent if true, delete the property in the referenced schemas to avoid duplicates
1656+ *
1657+ * @return the name
1658+ */
1659+ protected String getDiscriminatorValue (String refSchema , String discriminatorPropertyName , boolean propertyAlreadyPresent , Set <Schema > visitedSchemas ) {
1660+ String schemaName = ModelUtils .getSimpleRef (refSchema );
1661+ Schema schema = ModelUtils .getSchema (openAPI , schemaName );
1662+ Schema property = findProperty (schema , discriminatorPropertyName , propertyAlreadyPresent , visitedSchemas );
1663+ if (schema != null && schema .getExtensions () != null ) {
1664+ Object discriminatorValue = schema .getExtensions ().get ("x-discriminator-value" );
1665+ if (discriminatorValue != null ) {
1666+ return discriminatorValue .toString ();
1667+ }
1668+ }
1669+
1670+ // find the discriminator value as a unique enum value
1671+ property = ModelUtils .getReferencedSchema (openAPI , property );
1672+ if (property != null ) {
1673+ List enums = property .getEnum ();
1674+ if (enums != null && enums .size () == 1 ) {
1675+ return enums .get (0 ).toString ();
1676+ }
1677+ }
1678+
1679+ return schemaName ;
1680+ }
1681+
1682+ /**
1683+ * find a property under the schema.
1684+ *
1685+ * @param schema
1686+ * @param propertyName property to find
1687+ * @param toDelete if true delete the found property
1688+ * @param visitedSchemas avoid infinite recursion
1689+ * @return found property or null if not found.
1690+ */
1691+ private Schema findProperty (Schema schema , String propertyName , boolean toDelete , Set <Schema > visitedSchemas ) {
1692+ schema = ModelUtils .getReferencedSchema (openAPI , schema );
1693+ if (propertyName == null || schema == null || visitedSchemas .contains (schema )) {
1694+ return null ;
1695+ }
1696+ visitedSchemas .add (schema );
1697+ Map <String , Schema > properties = schema .getProperties ();
1698+ if (properties != null ) {
1699+ Schema property = ModelUtils .getReferencedSchema (openAPI , properties .get (propertyName ));
1700+ if (property != null ) {
1701+ if (toDelete ) {
1702+ if (schema .getProperties ().remove (propertyName ) != null ) {
1703+ LOGGER .info ("property " + propertyName + " has been removed in REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization" );
1704+ if (schema .getProperties ().isEmpty ()) {
1705+ schema .setProperties (null );
1706+ }
1707+ }
1708+ }
1709+ return property ;
1710+ }
1711+ }
1712+ List <Schema > allOfs = schema .getAllOf ();
1713+ if (allOfs != null ) {
1714+ for (Schema child : allOfs ) {
1715+ Schema found = findProperty (child , propertyName , toDelete , visitedSchemas );
1716+ if (found != null ) {
1717+ return found ;
1718+ }
1719+ }
1720+ }
1721+
1722+ return null ;
1723+ }
1724+
1725+
1726+ /**
1727+ * ensure that all schemas referenced in the discriminator mapping has an allOf to the parent schema.
1728+ *
1729+ * This allows DefaultCodeGen to detect inheritance.
1730+ *
1731+ * @param parent parent schma
1732+ * @param parentName name of the parent schema
1733+ */
1734+ protected void ensureInheritanceForDiscriminatorMappings (Schema parent , String parentName ) {
1735+ Discriminator discriminator = parent .getDiscriminator ();
1736+ if (discriminator != null && discriminator .getMapping () != null ) {
1737+ for (String mapping : discriminator .getMapping ().values ()) {
1738+ String refSchemaName = ModelUtils .getSimpleRef (mapping );
1739+ Schema child = ModelUtils .getSchema (openAPI , refSchemaName );
1740+ if (child != null ) {
1741+ if (parentName != null ) {
1742+ ensureInheritanceForDiscriminatorMapping (parent , child , parentName , new HashSet <>());
1743+ }
1744+ }
1745+ }
1746+ }
1747+ }
1748+
1749+ /**
1750+ * If not already present, add in the child an allOf referencing the parent.
1751+ */
1752+ protected void ensureInheritanceForDiscriminatorMapping (Schema parent , Schema child , String parentName , Set <Schema > visitedSchemas ) {
1753+ String reference = "#/components/schemas/" + parentName ;
1754+ List <Schema > allOf = child .getAllOf ();
1755+ if (allOf != null ) {
1756+ if (isParentReferencedInChild (parent , child , reference , visitedSchemas )) {
1757+ // already done, so no need to add
1758+ return ;
1759+ }
1760+ Schema refToParent = new Schema <>().$ref (reference );
1761+ allOf .add (refToParent );
1762+ } else {
1763+ allOf = new ArrayList <>();
1764+ child .setAllOf (allOf );
1765+ Schema refToParent = new Schema <>().$ref (reference );
1766+ allOf .add (refToParent );
1767+ Map <String , Schema > childProperties = child .getProperties ();
1768+ if (childProperties != null ) {
1769+ // move the properties inside the new allOf.
1770+ Schema newChildProperties = new Schema <>()
1771+ .properties (childProperties )
1772+ .additionalProperties (child .getAdditionalProperties ());
1773+ ModelUtils .copyMetadata (child , newChildProperties );
1774+ allOf .add (newChildProperties );
1775+ child .properties (null )
1776+ .type (null )
1777+ .additionalProperties (null )
1778+ .description (null )
1779+ ._default (null )
1780+ .deprecated (null )
1781+ .example (null )
1782+ .examples (null )
1783+ .readOnly (null )
1784+ .writeOnly (null )
1785+ .title (null );
1786+ }
1787+ }
1788+ }
1789+
1790+ /**
1791+ * return true if the child as an allOf referencing the parent schema.
1792+ */
1793+ private boolean isParentReferencedInChild (Schema parent , Schema child , String reference , Set <Schema > visitedSchemas ) {
1794+ if (child == null || visitedSchemas .contains (child )) {
1795+ return false ;
1796+ }
1797+ if (child .get$ref () != null && child .get$ref ().equals (reference )) {
1798+ return true ;
1799+ }
1800+ child = ModelUtils .getReferencedSchema (openAPI , child );
1801+ if (visitedSchemas .contains (child )) {
1802+ return false ;
1803+ }
1804+ visitedSchemas .add (child );
1805+ List <Schema > allOf = child .getAllOf ();
1806+ if (allOf != null ) {
1807+ for (Schema schema : allOf ) {
1808+ if (isParentReferencedInChild (parent , schema , reference , visitedSchemas )) {
1809+ return true ;
1810+ }
1811+ }
1812+ }
1813+ return false ;
1814+ }
1815+
15671816 /**
15681817 * Set nullable to true in array/set if needed.
15691818 *
0 commit comments