Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,13 @@ Example:
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/required-properties.yaml -o /tmp/java-okhttp/ --openapi-normalizer REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT=true
```

- `REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING``: when set to true, oneOf is removed and is converted into mappings in a discriminator mapping.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

Example:
```
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g spring -i modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml -o /tmp/java-spring/ --openapi-normalizer REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING=true
```

- `FILTER`

The `FILTER` parameter allows selective inclusion of API operations based on specific criteria. It applies the `x-internal: true` property to operations that do **not** match the specified values, preventing them from being generated. Multiple filters can be separated by a semicolon.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public class OpenAPINormalizer {
// are removed as most generators cannot handle such case at the moment
final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY";

// when set to true, oneOf is removed and is converted into mappings in a discriminator mapping
final String REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING = "REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING";


// when set to true, oneOf/anyOf with either string or enum string as sub schemas will be simplified
// to just string
final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING";
Expand Down Expand Up @@ -214,6 +218,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM);
ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT);
ruleNames.add(SORT_MODEL_PROPERTIES);
ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING);

// rules that are default to true
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
Expand Down Expand Up @@ -639,6 +644,10 @@ protected void normalizeComponentsSchemas() {

// normalize the schemas
schemas.put(schemaName, normalizeSchema(schema, new HashSet<>()));

if (getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) {
ensureInheritanceForDiscriminatorMappings(schema, schemaName);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: ensureInheritanceForDiscriminatorMappings() is invoked on the pre-normalization schema instance, so any normalization path that returns a new Schema object makes the discriminator mapping changes apply to a discarded object.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java, line 649:

<comment>`ensureInheritanceForDiscriminatorMappings()` is invoked on the pre-normalization schema instance, so any normalization path that returns a new Schema object makes the discriminator mapping changes apply to a discarded object.</comment>

<file context>
@@ -639,6 +644,10 @@ protected void normalizeComponentsSchemas() {
                 schemas.put(schemaName, normalizeSchema(schema, new HashSet<>()));
+
+                if (getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) {
+                    ensureInheritanceForDiscriminatorMappings(schema, schemaName);
+                }
             }
</file context>
Fix with Cubic

}
}
}
}
Expand Down Expand Up @@ -1053,6 +1062,8 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
// simplify first as the schema may no longer be a oneOf after processing the rule below
schema = processSimplifyOneOf(schema);

schema = processReplaceOneOfByMapping(schema);

// if it's still a oneOf, loop through the sub-schemas
if (schema.getOneOf() != null) {
for (int i = 0; i < schema.getOneOf().size(); i++) {
Expand Down Expand Up @@ -1569,6 +1580,214 @@ protected Schema processSimplifyOneOf(Schema schema) {
return schema;
}


/**
* Ensure inheritance is correctly defined for OneOf and Discriminators.
*
* For schemas containing oneOf and discriminator.propertyName:
* <ul>
* <li>Create the mappings as $refs</li>
* <li>Remove OneOf</li>
* </ul>
*/
protected Schema processReplaceOneOfByMapping(Schema schema) {
if (!getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) {
return schema;
}
Discriminator discriminator = schema.getDiscriminator();
if (discriminator != null) {
boolean inlineSchema = isInlineSchema(schema);
if (inlineSchema) {
// the For referenced schemas, ensure that there is an allOf with this schema.
LOGGER.warn("Inline oneOf schema not supported by REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization");
return schema;
}
if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) {
Map<String, String> mappings = new TreeMap<>();
discriminator.setMapping(mappings);
List<Schema> oneOfs = schema.getOneOf();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
for (Schema oneOf: oneOfs) {
String refSchema = oneOf.get$ref();
if (refSchema != null) {
boolean hasProperty = findProperty(schema, discriminator.getPropertyName(), false, new HashSet<>()) != null;
String name = getDiscriminatorValue(refSchema, discriminator.getPropertyName(), hasProperty);
mappings.put(name, refSchema);
}
}
}
// remove oneOf and only keep the discriminator mapping
schema.setOneOf(null);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}

return schema;
}

private boolean isInlineSchema(Schema schema) {
if (openAPI.getComponents()!=null && openAPI.getComponents().getSchemas()!=null) {
int identity = System.identityHashCode(schema);
for (Schema componentSchema: openAPI.getComponents().getSchemas().values()) {
if (System.identityHashCode(componentSchema) == identity) {
return false;
}
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}
return true;
}

/**
* Best effort to retrieve a good discriminator value.
* By order of precedence:
* <ul>
* <li>x-discriminator-value</li>
* <li>single enum value for attribute used by the discriminator.propertyName</li>
* <li>hame of the schema</li>
* </ul>
*
* @param refSchema $ref value like #/components/schemas/Dog
* @param discriminatorPropertyName name of the property used in the discriminator mapping
* @param propertyAlreadyPresent if true, delete the property in the referenced schemas to avoid duplicates
*
* @return the name
*/
protected String getDiscriminatorValue(String refSchema, String discriminatorPropertyName, boolean propertyAlreadyPresent) {
String schemaName = ModelUtils.getSimpleRef(refSchema);
Schema schema = ModelUtils.getSchema(openAPI, schemaName);
Schema property = findProperty(schema, discriminatorPropertyName, propertyAlreadyPresent, new HashSet<>());
if (schema != null && schema.getExtensions() != null) {
Object discriminatorValue = schema.getExtensions().get("x-discriminator-value");
if (discriminatorValue != null) {
return discriminatorValue.toString();
}
}

// find the discriminator value as a unique enum value
if (property != null) {
List enums = property.getEnum();
if (enums != null && enums.size() == 1) {
return enums.get(0).toString();
}
}

return schemaName;
}

/**
* find a property under the schema.
*
* @param schema
* @param propertyName property to find
* @param toDelete if true delete the found property
* @param visitedSchemas avoid infinite recursion
* @return found property or null if not found.
*/
private Schema findProperty(Schema schema, String propertyName, boolean toDelete, HashSet<Object> visitedSchemas) {
if (propertyName == null || schema == null || visitedSchemas.contains(schema)) {
return null;
}
visitedSchemas.add(schema);
Map<String, Schema> properties = schema.getProperties();
if (properties != null) {
Schema property = properties.get(propertyName);
if (property != null) {
if (toDelete) {
if (schema.getProperties().remove(propertyName) != null && schema.getProperties().isEmpty()) {
schema.setProperties(null);
}
}
return property;
}
}
List<Schema> allOfs = schema.getAllOf();
if (allOfs != null) {
for (Schema child : allOfs) {
Schema found = findProperty(child, propertyName, toDelete, visitedSchemas);
if (found != null) {
return found;
}
}
}

return null;
}


/**
* ensure that all schemas referenced in the discriminator mapping has an allOf to the parent schema.
*
* This allows DefaultCodeGen to detect inheritance.
*
* @param parent parent schma
* @param parentName name of the parent schema
*/
protected void ensureInheritanceForDiscriminatorMappings(Schema parent, String parentName) {
Discriminator discriminator = parent.getDiscriminator();
if (discriminator != null && discriminator.getMapping() != null) {
for (String mapping : discriminator.getMapping().values()) {
String refSchemaName = ModelUtils.getSimpleRef(mapping);
Schema child = ModelUtils.getSchema(openAPI, refSchemaName);
if (child != null) {
if (parentName != null) {
ensureInheritanceForDiscriminatorMappings(parent, child, parentName, new HashSet<>());
}
}
}
}
}

/**
* If not already present, add in the child an allOf referencing the parent.
*/
protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema child, String parentName, Set<Schema> visitedSchemas) {
String reference = "#/components/schemas/" + parentName;
List<Schema> allOf = child.getAllOf();
if (allOf != null) {
if (hasParent(parent, child, reference, visitedSchemas)) {
// already done, so no need to add
return;
}
Schema refToParent = new Schema<>().$ref(reference);
allOf.add(refToParent);
} else {
allOf = new ArrayList<>();
child.setAllOf(allOf);
Schema refToParent = new Schema<>().$ref(reference);
allOf.add(refToParent);
Map<String, Schema> childProperties = child.getProperties();
if (childProperties != null) {
// move the properties inside the new allOf.
Schema newChildProperties = new Schema<>().properties(childProperties);
allOf.add(newChildProperties);
child.setProperties(null);
child.setType(null);
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
}
}

/**
* return true if the child as an allOf referencing the parent scham.
*/
private boolean hasParent(Schema parent, Schema child, String reference, Set<Schema> visitedSchemas) {
if (child == null) {
return false;
}
if (child.get$ref() != null && child.get$ref().equals(reference)) {
return true;
}
List<Schema> allOf = child.getAllOf();
if (allOf != null) {
for (Schema schema : allOf) {
if (visitedSchemas.contains(schema)) {
return false;
}
visitedSchemas.add(schema);
if (hasParent(parent, schema, reference, visitedSchemas)) {
return true;
}
}
}
return false;
}

/**
* Set nullable to true in array/set if needed.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

package org.openapitools.codegen.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.util.Yaml;
import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
Expand Down Expand Up @@ -2645,4 +2647,20 @@ public LinkedHashSet<String> build() {
}
}
}


/*
* Simplest dump of an openApi contract on the console.
*
* Only use for debugging.
*/
public static void dumpAsYaml(OpenAPI openAPI) {
ObjectMapper mapper = Yaml.mapper();
try {
String yaml = mapper.writeValueAsString(openAPI);
System.out.println(yaml);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.openapitools.codegen.utils.ModelUtils;
import org.testng.annotations.Test;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.*;

Expand Down Expand Up @@ -1502,4 +1501,47 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
}
}

@Test
public void testREPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/oneOf_issue_23527.yaml");

Map<String, String> inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true");
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules);
openAPINormalizer.normalize();

Schema geoJsonObject = openAPI.getComponents().getSchemas().get("GeoJsonObject");
Map<String, String> mapping = geoJsonObject.getDiscriminator().getMapping();
assertEquals(mapping, Map.of("MultiPolygon", "#/components/schemas/Multi-Polygon", "Polygon", "#/components/schemas/Polygon" ));
}

@Test
public void issue_14769() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/oneOf_issue_14769.yaml");
Map<String, String> inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true");
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules);
openAPINormalizer.normalize();
// ModelUtils.dumpAsYaml(openAPI);
Schema vehicle = openAPI.getComponents().getSchemas().get("Vehicle");
Map<String, String> mapping = vehicle.getDiscriminator().getMapping();
assertEquals(mapping, Map.of("car", "#/components/schemas/Car", "plane", "#/components/schemas/Plane" ));
Schema car = openAPI.getComponents().getSchemas().get("Car");
assertNull(car.getProperties());
assertEquals(car.getAllOf().size(), 2);
assertEquals(((Schema)car.getAllOf().get(0)).get$ref(), "#/components/schemas/Vehicle");
assertEquals(((Schema)car.getAllOf().get(1)).getProperties().size(), 1);
assertEquals(((Schema)car.getAllOf().get(1)).getProperties().keySet(), Set.of("has_4_wheel_drive"));
}

@Test
public void oneOf_issue_23276() {
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/oneOf_issue_23276.yaml");
Map<String, String> inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true");
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules);
openAPINormalizer.normalize();
// ModelUtils.dumpAsYaml(openAPI);
Schema payload = (Schema)openAPI.getComponents().getSchemas().get("DeviceLifecycleEvent").getProperties().get("payload");
// inline oneOf are not converted
assertNotNull(payload.getOneOf());
}

}
Loading
Loading