Skip to content

[Java] [Spring] mixed OneOf support and jackson JsonUnwrapped#23761

Open
jpfinne wants to merge 13 commits into
OpenAPITools:masterfrom
jpfinne:feature/inlineOneOfWithProperties
Open

[Java] [Spring] mixed OneOf support and jackson JsonUnwrapped#23761
jpfinne wants to merge 13 commits into
OpenAPITools:masterfrom
jpfinne:feature/inlineOneOfWithProperties

Conversation

@jpfinne
Copy link
Copy Markdown
Contributor

@jpfinne jpfinne commented May 11, 2026

Fix #23759

Support models with oneOf without discriminator combined with properties (or allOf)

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

java | @bbdouglas (2017/07) @sreeshas (2017/08) @jfiala (2017/08) @lukoyanov (2017/09) @cbornet (2017/09) @jeff9finger (2018/01) @karismann (2019/03) @Zomzog (2019/04) @lwlee2608 (2019/10) @martin-mfg (2023/08)
Java Spring | @cachescrubber (2022/02) @welshm (2022/02) @MelleD (2022/02) @atextor (2022/02) @manedev79 (2022/02) @javisst (2022/02) @borsch (2022/02) @banlevente (2022/02) @Zomzog (2022/09) @martin-mfg (2023/08)
Please review


Summary by cubic

Adds optional support for inline oneOf combined with properties/allOf (no discriminator) in Java and Spring generators by moving oneOf into a @JsonUnwrapped field. When oneOf interfaces are enabled, a wrapper interface with a @JsonCreator and Jackson mixins ensures correct (de)serialization. Fixes #23759.

  • New Features

    • New option useWrapperForMixedOneOf (default false) in java, java-microprofile, spring, and java-camel.
    • When enabled (and no discriminator): inline oneOf is moved to <SchemaName>OneOfWrapper and exposed as a oneOf field annotated with @JsonUnwrapped.
    • Works with or without useOneOfInterfaces:
      • With interfaces: the wrapper is an interface; adds @JsonCreator valueOf(JsonNode) and an inner mixin using @JsonTypeInfo(Id.DEDUCTION)/@JsonSubTypes; generates JacksonMixinConfig to register mixins.
      • Without interfaces: the wrapper is a class; no mixins or @JsonCreator.
    • Supports Jackson 2 and 3; updates import mappings for JsonNode, JsonMapper, and JsonUnwrapped; hooks mixins via JsonMapper (Jackson 3) or ObjectMapper (Jackson 2).
    • Updates templates and tests for Java client and Spring Boot 3/4; docs list the new option across generators.
  • Bug Fixes

    • Avoids a possible NPE when preprocessing oneOf schemas during interface generation.

Written for commit fd72fd3. Summary will update on new commits.

@jpfinne jpfinne marked this pull request as ready for review May 11, 2026 18:21
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/Java/jacksonMixinConfig.mustache:19">
P1: Unsynchronized lazy initialization can overwrite a concurrently supplied custom mapper and make mapper configuration nondeterministic.</violation>
</file>

<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java:2872">
P1: Unconditionally wrapping `importMapping.get("JsonNode")` in `Map.of(...)` can NPE when that mapping is absent.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@@ -0,0 +1,44 @@
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>

.add(cm.classname);

cm.getVendorExtensions().put("x-oneof-jsonCreator", true);
obj.getImports().add(Map.of("import", importMapping.get("JsonNode")));
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: Unconditionally wrapping importMapping.get("JsonNode") in Map.of(...) can NPE when that mapping is absent.

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/languages/AbstractJavaCodegen.java, line 2872:

<comment>Unconditionally wrapping `importMapping.get("JsonNode")` in `Map.of(...)` can NPE when that mapping is absent.</comment>

<file context>
@@ -2809,4 +2814,61 @@ protected void addNullableImportForOperation(CodegenOperation codegenOperation)
+                .add(cm.classname);
+
+        cm.getVendorExtensions().put("x-oneof-jsonCreator", true);
+        obj.getImports().add(Map.of("import", importMapping.get("JsonNode")));
+    }
 }
</file context>
Suggested change
obj.getImports().add(Map.of("import", importMapping.get("JsonNode")));
String jsonNodeImport = importMapping.get("JsonNode");
if (jsonNodeImport != null) {
obj.getImports().add(Map.of("import", jsonNodeImport));
}

@jpfinne jpfinne marked this pull request as draft May 11, 2026 20:00
@jpfinne jpfinne changed the title [Java] [Spring] mixed OneOf support with inheritance and JsonUnwrapped [Java] [Spring] mixed OneOf support and JsonUnwrapped May 11, 2026
@jpfinne jpfinne changed the title [Java] [Spring] mixed OneOf support and JsonUnwrapped [Java] [Spring] mixed OneOf support and jackson JsonUnwrapped May 11, 2026
@jpfinne jpfinne marked this pull request as ready for review May 11, 2026 22:14
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java:2751">
P2: Reads `x-one-of-name` from the generator-level `vendorExtensions` map instead of the current schema, so the schema-scoped oneOf name added during preprocessing is ignored.</violation>
</file>

<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java:2830">
P1: Wrapper schema names are deterministic and inserted without any collision check, so this can overwrite an existing component schema.</violation>

<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java:2836">
P2: Hardcoded synthetic property name "oneOf" can silently overwrite an existing schema property with the same name</violation>

<violation number="3" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java:2872">
P2: Unconditional `Map.of` call can throw if `JsonNode` is not mapped for this generator.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

newOneOfSchema.addExtension("x-unwrappedOneOf", true);
String nOneOf = toModelName(schemaName + "OneOf");
String newSchemaName = nOneOf+ "_wrapper";
openAPI.getComponents().getSchemas().put(newSchemaName, newOneOfSchema);
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: Wrapper schema names are deterministic and inserted without any collision check, so this can overwrite an existing component schema.

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/languages/AbstractJavaCodegen.java, line 2830:

<comment>Wrapper schema names are deterministic and inserted without any collision check, so this can overwrite an existing component schema.</comment>

<file context>
@@ -2809,4 +2814,61 @@ protected void addNullableImportForOperation(CodegenOperation codegenOperation)
+            newOneOfSchema.addExtension("x-unwrappedOneOf", true);
+            String nOneOf = toModelName(schemaName + "OneOf");
+            String newSchemaName = nOneOf+ "_wrapper";
+            openAPI.getComponents().getSchemas().put(newSchemaName, newOneOfSchema);
+            Schema newSchemaRef = new Schema().$ref("#/components/schemas/" + newSchemaName);
+            s.oneOf(null);
</file context>

oneOf.oneOf(composed.getOneOf());
composed.oneOf(null);

String oneOfName = (String)vendorExtensions.get("x-one-of-name");
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: Reads x-one-of-name from the generator-level vendorExtensions map instead of the current schema, so the schema-scoped oneOf name added during preprocessing is ignored.

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/DefaultCodegen.java, line 2751:

<comment>Reads `x-one-of-name` from the generator-level `vendorExtensions` map instead of the current schema, so the schema-scoped oneOf name added during preprocessing is ignored.</comment>

<file context>
@@ -2710,13 +2738,23 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
+                oneOf.oneOf(composed.getOneOf());
+                composed.oneOf(null);
+
+                String oneOfName = (String)vendorExtensions.get("x-one-of-name");
+                oneOfName = oneOfName != null ? "oneOf"+ oneOfName: "oneOf";
+                addVars(m, Map.of(oneOfName, oneOf), List.of(), null, null);
</file context>
Suggested change
String oneOfName = (String)vendorExtensions.get("x-one-of-name");
String oneOfName = composed.getExtensions() != null ? (String) composed.getExtensions().get(X_ONE_OF_NAME) : null;

.add(cm.classname);

cm.getVendorExtensions().put("x-oneof-jsonCreator", true);
obj.getImports().add(Map.of("import", importMapping.get("JsonNode")));
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: Unconditional Map.of call can throw if JsonNode is not mapped for this generator.

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/languages/AbstractJavaCodegen.java, line 2872:

<comment>Unconditional `Map.of` call can throw if `JsonNode` is not mapped for this generator.</comment>

<file context>
@@ -2809,4 +2814,61 @@ protected void addNullableImportForOperation(CodegenOperation codegenOperation)
+                .add(cm.classname);
+
+        cm.getVendorExtensions().put("x-oneof-jsonCreator", true);
+        obj.getImports().add(Map.of("import", importMapping.get("JsonNode")));
+    }
 }
</file context>
Suggested change
obj.getImports().add(Map.of("import", importMapping.get("JsonNode")));
obj.getImports().add(Map.of("import", importMapping.getOrDefault("JsonNode", "com.fasterxml.jackson.databind.JsonNode")));

// TODO: configuration of the property name
String propertyName = "oneOf";
if (ModelUtils.hasProperties(s)) {
s.getProperties().put(propertyName, newSchemaRef);
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: Hardcoded synthetic property name "oneOf" can silently overwrite an existing schema property with the same name

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/languages/AbstractJavaCodegen.java, line 2836:

<comment>Hardcoded synthetic property name "oneOf" can silently overwrite an existing schema property with the same name</comment>

<file context>
@@ -2809,4 +2814,61 @@ protected void addNullableImportForOperation(CodegenOperation codegenOperation)
+            // TODO: configuration of the property name
+            String propertyName = "oneOf";
+            if (ModelUtils.hasProperties(s)) {
+                s.getProperties().put(propertyName, newSchemaRef);
+            } else if (ModelUtils.hasAllOf(s)) {
+                Schema schema = new Schema();
</file context>

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-unwrappedOneOf")) {
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.

Could we add this to the vendor extensions that are defined in CodegenConstants to encourage reuse of vendor extensions between language implementations.

}

@Override
protected void preprocessMixedOneOf(Schema s, String schemaName) {
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.

Should this discriminator logic be moved out into a separate component to highlight that it might be of "general interest" between languages rather than strictly tied to Java?

I find that the library is now starting to support a lot of scenarios, and especially that Java spearheads those changes with your contributions. I believe that it would be preferable if the logic for handling these scenarios was generalized directly if possible.

Copy link
Copy Markdown
Contributor Author

@jpfinne jpfinne May 12, 2026

Choose a reason for hiding this comment

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

@Mattias-Sehlstedt initially I put the logic as an OpenapiNormalizer. It was in fact simpler than finding the right place in the DefaultCodeGen.

Proposal:

  • The normalizer implements the logic of AbstractJavaCodeGen.preprocessMixedOneOf with a new rule USE_WRAPPER_FOR_MIXED_ONE_OF
  • Remove the option from the java generators. The generators detect the constant x-unwrappedOneOf and configure the imports and the correct vendor extensions (also using constants)
  • moving a few common functionalities from JavaClientCodeGen and SpringCodeGen to AbstractJavaCodeGen (isJackson3(), getConfigOrInvokerPackage()....)

What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REQ] [Java] [Spring] mixed OneOf support with JsonUnwrapped

2 participants