Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2e9ba37
wip
jpfinne May 7, 2026
5a4dd70
Merge remote-tracking branch 'refs/remotes/origin/master' into featur…
jpfinne May 8, 2026
a6f9281
useWrapperForMixedOneOf
jpfinne May 10, 2026
cd2a1c5
useWrapperForMixedOneOf
jpfinne May 11, 2026
c132be4
regenerate doc
jpfinne May 11, 2026
59c67f2
Merge pull request #4 from OpenAPITools/master
jpfinne May 11, 2026
a286059
Merge branch 'master' into feature/inlineOneOfWithProperties
jpfinne May 11, 2026
32eb110
regenerate doc
jpfinne May 11, 2026
d38d951
Support for useOneOfInterfaces=false
jpfinne May 11, 2026
ceb1af9
Better backward compatiblity
jpfinne May 11, 2026
536d359
Avoid NPE
jpfinne May 11, 2026
822085e
Better backward compatiblity
jpfinne May 11, 2026
fd72fd3
test for interface/normalClass
jpfinne May 11, 2026
784da69
Merge branch 'OpenAPITools:master' into master
jpfinne May 12, 2026
621c3da
Merge branch 'refs/heads/master' into feature/inlineOneOfWithProperties
jpfinne May 12, 2026
2f06df2
Move the cliOption to the OpenAPINormalizer
jpfinne May 14, 2026
9ee6075
Merge branch 'OpenAPITools:master' into master
jpfinne May 14, 2026
e04af45
Normalize mixed content
jpfinne May 14, 2026
997b859
Rename USE_UNWRAPPED_FOR_COMPOSITE_ONEOF
jpfinne May 14, 2026
5f0bfe1
Rename method to processUnwrapCompositeOneOf
jpfinne May 14, 2026
5ebc7e2
Move more constants to CodegenConstants
jpfinne May 14, 2026
6f046f0
Update modules/openapi-generator/src/main/java/org/openapitools/codeg…
jpfinne May 15, 2026
ed075ce
use function skipUnwrapOneOf
jpfinne May 15, 2026
91ce7ba
Merge branch 'OpenAPITools:master' into master
jpfinne May 15, 2026
aab97db
Merge branch 'master' into feature/inlineOneOfWithProperties
jpfinne May 15, 2026
f032a06
Force rebuild
jpfinne May 16, 2026
0808d0e
Merge branch 'OpenAPITools:master' into master
jpfinne May 17, 2026
46d9c9c
Merge branch 'OpenAPITools:master' into master
jpfinne May 18, 2026
462e147
Merge branch 'master' into feature/inlineOneOfWithProperties
jpfinne May 18, 2026
441727c
merge master
jpfinne May 18, 2026
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
41 changes: 41 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,47 @@ index 6f27abd..146c61c 100644
type: string
```

- `USE_UNWRAPPED_FOR_INLINE_ONEOF` set to true to unwrap inline oneOf combined with allOf/properties and without discriminator (aka mixed oneOf). Set the vendorExtension X_ONE_OF_UNWRAPPED to be used by generators. For example the java generator annotates the new oneOf property with the jackson `@JsonUnwrapped` annotation

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

Here is what the change in the spec looks like:
```diff
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 6f27abd..146c61c 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -9,10 +9,10 @@ components:
schemas:
Account:
- oneOf:
- - properties:
- bankNumber:
- type: string
- bic:
- type: string
- - properties:
- iban:
- type: string
properties:
bank:
$ref: '#/components/schemas/Bank'
+ oneOf:
+ X_ONE_OF_UNWRAPPED: true
+ oneOf:
+ - properties:
+ bankNumber:
+ type: string
+ bic:
+ type: string
+ - properties:
+ iban:
+ type: string
```

- `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 @@ -499,4 +499,7 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case,
public static final String X_DISCRIMINATOR_VALUE = "x-discriminator-value";
public static final String X_ONE_OF_NAME = "x-one-of-name";
public static final String X_NULLABLE = "x-nullable";

public static final String X_ONE_OF_JSON_CREATOR = "x-oneof-jsonCreator";
public static final String X_ONE_OF_UNWRAPPED = "x-oneOfunwrapper";
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public class OpenAPINormalizer {
// when set to true, sort model properties by name to ensure deterministic output
final String SORT_MODEL_PROPERTIES = "SORT_MODEL_PROPERTIES";

final String USE_UNWRAPPED_FOR_INLINE_ONEOF = "USE_UNWRAPPED_FOR_INLINE_ONEOF";
// ============= end of rules =============

/**
Expand Down Expand Up @@ -219,6 +220,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT);
ruleNames.add(SORT_MODEL_PROPERTIES);
ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING);
ruleNames.add(USE_UNWRAPPED_FOR_INLINE_ONEOF);

// rules that are default to true
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
Expand Down Expand Up @@ -770,7 +772,7 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
}

if (ModelUtils.hasAllOf(schema)) {
return normalizeAllOf(schema, visitedSchemas);
schema = normalizeAllOf(schema, visitedSchemas);
}

if (ModelUtils.hasOneOf(schema)) {
Expand Down Expand Up @@ -1079,6 +1081,8 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
schema.getOneOf().set(i, normalizeSchema((Schema) item, visitedSchemas));
}
schema = processReplaceOneOfByMapping(schema);
schema = processUnwrappedOneOf(schema);

} else {
// normalize it as it's no longer an oneOf
schema = normalizeSchema(schema, visitedSchemas);
Expand All @@ -1087,6 +1091,49 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
return schema;
}

protected Schema processUnwrappedOneOf(Schema schema) {
if (!getRule(USE_UNWRAPPED_FOR_INLINE_ONEOF)) {
return schema;
}
if (!(ModelUtils.hasOneOf(schema) && (ModelUtils.hasProperties(schema) || ModelUtils.hasAllOf(schema)))) {
return schema;
}

// skip handling of oneOf + properties + discriminator
// accept discriminator mappings NOT matching the oneOf elements.
Discriminator discriminator = schema.getDiscriminator();
boolean hasDiscriminator = schema.getDiscriminator() != null;
if (hasDiscriminator) {
List<Schema> oneOfs = schema.getOneOf();
if (oneOfs.stream().allMatch(oneOf -> oneOf.get$ref() != null)) {
Comment thread
jpfinne marked this conversation as resolved.
Outdated
// skip normalization if discriminator but not maping
if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) {
return schema;
}
// if same size for mapping and oneOf, assume that we skip this normalization
if (discriminator.getMapping() != null && discriminator.getMapping().size() == schema.getOneOf().size()) {
return schema;
}
}
}

Schema newOneOfSchema = new ComposedSchema();
newOneOfSchema.oneOf(new ArrayList<>(schema.getOneOf()));
newOneOfSchema.addExtension(X_ONE_OF_UNWRAPPED, true);
schema.oneOf(null);
// TODO: configuration of the property name
String propertyName = "oneOf";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Synthetic oneOf property can silently overwrite an existing user-defined oneOf property

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 1125:

<comment>Synthetic `oneOf` property can silently overwrite an existing user-defined `oneOf` property</comment>

<file context>
@@ -1087,6 +1091,49 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
+        newOneOfSchema.addExtension(X_ONE_OF_UNWRAPPED, true);
+        schema.oneOf(null);
+        // TODO: configuration of the property name
+        String propertyName = "oneOf";
+        if (ModelUtils.hasProperties(schema)) {
+            schema.getProperties().put(propertyName, newOneOfSchema);
</file context>

if (ModelUtils.hasProperties(schema)) {
schema.getProperties().put(propertyName, newOneOfSchema);
} else if (ModelUtils.hasAllOf(schema)) {
Schema allOfSchema = new Schema();
allOfSchema.addProperty(propertyName, newOneOfSchema);
schema.getAllOf().add(allOfSchema);
}

return schema;
}

protected Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
//transform anyOf into enums if needed
schema = processSimplifyAnyOfEnum(schema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.servers.Server;
Expand Down Expand Up @@ -75,8 +72,8 @@
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static org.openapitools.codegen.CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES;
import static org.openapitools.codegen.CodegenConstants.X_IMPLEMENTS;
import static org.openapitools.codegen.CodegenConstants.*;
import static org.openapitools.codegen.VendorExtension.X_FIELD_EXTRA_ANNOTATION;
import static org.openapitools.codegen.utils.CamelizeOption.*;
import static org.openapitools.codegen.utils.ModelUtils.getSchemaItems;
import static org.openapitools.codegen.utils.OnceLogger.once;
Expand Down Expand Up @@ -228,6 +225,8 @@ protected enum ENUM_PROPERTY_NAMING_TYPE {MACRO_CASE, legacy, original}
protected JSpecifyNullableLambda jSpecifyNullableLambda;
@Getter @Setter
protected boolean useDeductionForOneOfInterfaces = false;
@Getter @Setter
protected boolean useWrapperForMixedOneOf;

private Map<String, String> schemaKeyToModelNameCache = new HashMap<>();

Expand Down Expand Up @@ -646,6 +645,7 @@ public void processOpts() {
importMapping.put("JsonIgnore", "com.fasterxml.jackson.annotation.JsonIgnore");
importMapping.put("JsonIgnoreProperties", "com.fasterxml.jackson.annotation.JsonIgnoreProperties");
importMapping.put("JsonInclude", "com.fasterxml.jackson.annotation.JsonInclude");
importMapping.put("JsonUnwrapped", "com.fasterxml.jackson.annotation.JsonUnwrapped");
if (openApiNullable) {
importMapping.put("JsonNullable", "org.openapitools.jackson.nullable.JsonNullable");
}
Expand Down Expand Up @@ -2013,6 +2013,10 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
if (property.dataType != null && property.dataType.equals(property.name) && property.dataType.toUpperCase(Locale.ROOT).equals(property.name)) {
property.name = property.name.toLowerCase(Locale.ROOT);
}
if (property.getVendorExtensions().containsKey(X_ONE_OF_UNWRAPPED)) {
model.imports.add("JsonUnwrapped");
property.getVendorExtensions().put(X_FIELD_EXTRA_ANNOTATION.getName(), "@JsonUnwrapped");
}
}

@Override
Expand Down Expand Up @@ -2668,7 +2672,7 @@ public List<VendorExtension> getSupportedVendorExtensions() {
extensions.add(VendorExtension.X_ACCEPTS);
extensions.add(VendorExtension.X_CONTENT_TYPE);
extensions.add(VendorExtension.X_CLASS_EXTRA_ANNOTATION);
extensions.add(VendorExtension.X_FIELD_EXTRA_ANNOTATION);
extensions.add(X_FIELD_EXTRA_ANNOTATION);
return extensions;
}

Expand Down Expand Up @@ -2813,4 +2817,70 @@ protected void addNullableImportForOperation(CodegenOperation codegenOperation)
.findAny()
.ifPresent(param -> codegenOperation.imports.add("Nullable"));
}

@Override
public Map<String, ModelsMap> updateAllModels(Map<String, ModelsMap> objs) {
objs = super.updateAllModels(objs);
if (jackson && useOneOfInterfaces) {
// handling of X_ONE_OF_UNWRAPPED with inheritance
for (ModelsMap obj : objs.values()) {
for (ModelMap mo : obj.getModels()) {
CodegenModel cm = mo.getModel();
if (cm.getVendorExtensions().containsKey(X_ONE_OF_UNWRAPPED) && cm.getInterfaceModels() != null) {
addOneOfMixinSupport(obj, cm);
}
}
}
}
return objs;
}

/**
* Add JsonCreator and mixin interface to the oneOf interface.
* <p>
* Add the necessary imports.
* <p>
* Construct a vendorExtension X_ONE_OF_JSON_CREATOR with a map containing:
* <ol>
* <li>>mapper: JsonMapper or ObjectMapper
* <li>mixins: a list with the class name of the oneOf classes
* </ol>
*/
protected void addOneOfMixinSupport(ModelsMap obj, CodegenModel cm) {
String configPackage = getConfigPackage();
Map<String, Object> config;
if (supportingFiles.stream().noneMatch(sf -> "JacksonMixinConfig.java".equals(sf.getDestinationFilename()))) {
supportingFiles.add(new SupportingFile("jacksonMixinConfig.mustache",
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator),
"JacksonMixinConfig.java"));
config = Map.of(
"mapper", isUseJackson3() ? "JsonMapper" : "ObjectMapper",
"mixins", new ArrayList<>());
vendorExtensions.put("x-jacksonMixinConfig", config);
} else {
config = ( Map<String, Object>)vendorExtensions.get("x-jacksonMixinConfig");
}
cm.vendorExtensions.put(X_ONE_OF_JSON_CREATOR, config);
((List)config.get("mixins")).add(cm.classname);

if (!isUseJackson3()) {
obj.getImports().add(Map.of("import", "com.fasterxml.jackson.core.JsonProcessingException"));
}
obj.getImports().add(Map.of("import", importMapping.get("JsonNode")));
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Comment thread
jpfinne marked this conversation as resolved.
Outdated
obj.getImports().add(Map.of("import", configPackage + ".JacksonMixinConfig"));

}

/*
* return the config package.
*
* by default use invokerPackage
*/
protected String getConfigPackage() {
return invokerPackage;
}

protected boolean isUseJackson3() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1322,10 +1322,14 @@ public void setCaseInsensitiveResponseHeaders(final Boolean caseInsensitiveRespo

protected void applyJackson2Package() {
writePropertyBack(JACKSON_PACKAGE, JACKSON2_PACKAGE);
importMapping.put("JsonNode", JACKSON2_PACKAGE + ".databind.JsonNode");
importMapping.put("JsonMapper", JACKSON2_PACKAGE + ".databind.json.JsonMapper");
}

protected void applyJackson3Package() {
writePropertyBack(JACKSON_PACKAGE, JACKSON3_PACKAGE);
importMapping.put("JsonNode", JACKSON3_PACKAGE + ".databind.JsonNode");
importMapping.put("JsonMapper", JACKSON3_PACKAGE + ".databind.json.JsonMapper");
}

public void setSerializationLibrary(String serializationLibrary) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ public SpringCodegen() {
.defaultValue("false")
);
cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks", useJspecify));

supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
Expand Down Expand Up @@ -605,6 +606,10 @@ public void processOpts() {
importMapping.put("org.springframework.core.io.Resource", "org.springframework.core.io.Resource");
importMapping.put("DateTimeFormat", "org.springframework.format.annotation.DateTimeFormat");
importMapping.put("ParameterObject", "org.springdoc.api.annotations.ParameterObject");
String jacksonPackage = (String)additionalProperties.get("jacksonPackage");
importMapping.put("JsonNode", jacksonPackage + ".databind.JsonNode");
importMapping.put("JsonMapper", jacksonPackage + ".databind.json.JsonMapper");

if (isUseSpringBoot3() || isUseSpringBoot4()) {
importMapping.put("ParameterObject", "org.springdoc.core.annotations.ParameterObject");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package {{invokerPackage}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Unsynchronized lazy initialization can overwrite a concurrently supplied custom mapper and make mapper configuration nondeterministic.

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/resources/Java/jacksonMixinConfig.mustache, line 19:

<comment>Unsynchronized lazy initialization can overwrite a concurrently supplied custom mapper and make mapper configuration nondeterministic.</comment>

<file context>
@@ -0,0 +1,44 @@
+      Get the {{vendorExtensions.x-jackson-mixins-mapper}} used by the @JsonCreator in @JsonUnWrapped interfaces.
+    */
+    public static {{vendorExtensions.x-jackson-mixins-mapper}} getMapper() {
+      if (INSTANCE == null) {
+        setBuilder({{^useJackson3}}JsonMapper.builder().findAndAddModules(){{/useJackson3}}{{#useJackson3}}JsonMapper.shared().rebuild(){{/useJackson3}});
+      }
</file context>

{{#vendorExtensions.x-jacksonMixinConfig}}

{{^useJackson3}}
import {{jacksonPackage}}.databind.ObjectMapper;
{{/useJackson3}}
import {{jacksonPackage}}.databind.json.JsonMapper;
{{#mixins}}
import {{modelPackage}}.{{.}};
{{/mixins}}

public class JacksonMixinConfig {
private static volatile {{mapper}} INSTANCE;

/**
Get the {{mapper}} used by the @JsonCreator in @JsonUnWrapped interfaces.
*/
public static {{mapper}} getMapper() {
if (INSTANCE == null) {
setBuilder({{^useJackson3}}JsonMapper.builder().findAndAddModules(){{/useJackson3}}{{#useJackson3}}JsonMapper.shared().rebuild(){{/useJackson3}});
}
return INSTANCE;
}

/**
Initialize the mapper used by the @JsonCreator.
<p>
Do not pass the global {{mapper}} as @JsonUnwrapped does not support deserializer.
*/
public static void set{{mapper}}({{mapper}} mapper) {
INSTANCE = mapper;
}

/**
Initialize the mapper used by the @JsonCreator.
<p>
Configure the Mixins
*/
public static void setBuilder(JsonMapper.Builder jsonMapperBuilder) {
INSTANCE = jsonMapperBuilder{{#mixins}}
.addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/mixins}}
.build();
}
{{/vendorExtensions.x-jacksonMixinConfig}}
Comment thread
jpfinne marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,21 @@ public {{>sealed}}interface {{classname}} {{#vendorExtensions.x-implements}}{{#-
{{#discriminator}}
public {{propertyType}} {{propertyGetter}}();
{{/discriminator}}
{{#vendorExtensions.x-oneof-jsonCreator}}

@JsonCreator
static {{classname}} valueOf(JsonNode node) {{^useJackson3}}throws JsonProcessingException {{/useJackson3}}{
return JacksonMixinConfig.getMapper().treeToValue(node, {{classname}}.class);
}

@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
{{#interfaceModels}}
@JsonSubTypes.Type(value = {{classname}}.class){{^-last}}, {{/-last}}
{{/interfaceModels}}
})
static interface {{classname}}Mixin {

}
{{/vendorExtensions.x-oneof-jsonCreator}}
}
Loading
Loading