Skip to content

[Java] [Spring] OpenAPI normalizer USE_UNWRAPPED_FOR_COMPOSITE_ONEOF for mixed OneOf support and jackson JsonUnwrapped#23761

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

[Java] [Spring] OpenAPI normalizer USE_UNWRAPPED_FOR_COMPOSITE_ONEOF for mixed OneOf support and jackson JsonUnwrapped#23761
jpfinne wants to merge 20 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 mixed with properties/allOf (no discriminator) in Java and Spring by nesting the union under a new oneOf property annotated with @JsonUnwrapped. When oneOf interfaces are enabled, a static @JsonCreator with Jackson mixins handles (de)serialization. Fixes #23759.

  • New Features

    • Enable via OpenAPI normalizer rule USE_UNWRAPPED_FOR_COMPOSITE_ONEOF (sets x-oneOfunwrapped); the original inline oneOf is moved under a new oneOf property which generators annotate with @JsonUnwrapped.
    • With useOneOfInterfaces=true: generate a valueOf(JsonNode) @JsonCreator, a nested mixin using @JsonTypeInfo(Id.DEDUCTION)/@JsonSubTypes, and a JacksonMixinConfig that registers mixins and provides a mapper.
    • With useOneOfInterfaces=false: generate a normal class (no mixins or @JsonCreator).
    • Supports Jackson 2 and 3; adds import mappings for JsonNode and JsonMapper; generates mixin config for both Java client and Spring Boot 3/4.
    • Centralizes Jackson and vendor extension constants in CodegenConstants (JACKSON2_PACKAGE, JACKSON3_PACKAGE, jacksonPackage, x-oneOfunwrapped, x-oneof-jsonCreator) to align generators.
  • Bug Fixes

    • Prevent a potential NPE when preprocessing oneOf schemas during interface generation.

Written for commit 5ebc7e2. 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>

@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.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Mattias-Sehlstedt now an OpenapiNormalizer. The downside is the naming convention (OneOf sufix)

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.

It being handled as a normalized I believe is generally preferred since then the codegen logic can be centralized towards "best practices" regarding how to express concepts with OAS.

For the suffix, is that technically something that would be possible to attached to the naming mappers? (e.g., the inline naming one). I have personally not used them myself since I have always avoided inline models.

jpfinne added 2 commits May 13, 2026 00:39
# Conflicts:
#	modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java
#	modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java
#	modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java
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.

1 issue found across 15 files (changes from recent commits).

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">
P1: NullPointerException risk when components.schemas is null in preprocessOpenAPI</violation>
</file>

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

@jpfinne jpfinne changed the title [Java] [Spring] mixed OneOf support and jackson JsonUnwrapped [Java] [Spring] OpenAPI normalizer USE_UNWRAPPED_FOR_COMPOSITE_ONEOF for mixed OneOf support and jackson JsonUnwrapped May 14, 2026
@jpfinne
Copy link
Copy Markdown
Contributor Author

jpfinne commented May 14, 2026

@cubic-dev-ai

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented May 14, 2026

@cubic-dev-ai

@jpfinne I have started the AI code review. It will take a few minutes to complete.

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.

3 issues found across 16 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:44">
P1: Class closing brace is outside the conditional Mustache section, producing invalid Java if the section is omitted</violation>
</file>

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

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java:1108">
P1: NullPointerException in processUnwrapCompositeOneOf when oneOf list contains null elements. normalizeOneOf explicitly tolerates null entries (`if (item == null) continue;`) but processUnwrapCompositeOneOf's stream allMatch dereferences elements without null checking (`oneOf.get$ref()`), causing an NPE.</violation>

<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java:1125">
P2: Synthetic `oneOf` property can silently overwrite an existing user-defined `oneOf` property</violation>
</file>

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

.addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/mixins}}
.build();
}
{{/vendorExtensions.x-jacksonMixinConfig}}
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: Class closing brace is outside the conditional Mustache section, producing invalid Java if the section is omitted

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

<comment>Class closing brace is outside the conditional Mustache section, producing invalid Java if the section is omitted</comment>

<file context>
@@ -0,0 +1,45 @@
+          .addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/mixins}}
+          .build();
+    }
+ {{/vendorExtensions.x-jacksonMixinConfig}}
+}
\ No newline at end of file
</file context>

boolean hasDiscriminator = schema.getDiscriminator() != null;
if (hasDiscriminator) {
List<Schema> oneOfs = schema.getOneOf();
if (oneOfs.stream().allMatch(oneOf -> oneOf.get$ref() != null)) {
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: NullPointerException in processUnwrapCompositeOneOf when oneOf list contains null elements. normalizeOneOf explicitly tolerates null entries (if (item == null) continue;) but processUnwrapCompositeOneOf's stream allMatch dereferences elements without null checking (oneOf.get$ref()), causing an NPE.

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

<comment>NullPointerException in processUnwrapCompositeOneOf when oneOf list contains null elements. normalizeOneOf explicitly tolerates null entries (`if (item == null) continue;`) but processUnwrapCompositeOneOf's stream allMatch dereferences elements without null checking (`oneOf.get$ref()`), causing an NPE.</comment>

<file context>
@@ -1087,6 +1091,49 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
+        boolean hasDiscriminator = schema.getDiscriminator() != null;
+        if (hasDiscriminator) {
+            List<Schema> oneOfs = schema.getOneOf();
+            if (oneOfs.stream().allMatch(oneOf -> oneOf.get$ref() != null)) {
+                // skip normalization if discriminator but not maping
+                if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) {
</file context>
Suggested change
if (oneOfs.stream().allMatch(oneOf -> oneOf.get$ref() != null)) {
if (oneOfs.stream().allMatch(oneOf -> oneOf != null && oneOf.get$ref() != null)) {

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>

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