diff --git a/datamodel/openapi/openapi-api-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/sample/model/OneOfWithMatrix.java b/datamodel/openapi/openapi-api-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/sample/model/OneOfWithMatrix.java new file mode 100644 index 000000000..45ddb0c62 --- /dev/null +++ b/datamodel/openapi/openapi-api-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/sample/model/OneOfWithMatrix.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved. + */ + +/* + * SodaStore API + * API for managing soda products and orders in SodaStore. + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package com.sap.cloud.sdk.datamodel.openapi.sample.model; + +import java.util.List; + +import javax.annotation.Nonnull; + +/** + * OneOfWithMatrix + */ +public interface OneOfWithMatrix +{ + /** + * Helper class to create a Integer that implements {@link OneOfWithMatrix}. + */ + record InnerInteger(@com.fasterxml.jackson.annotation.JsonValue @Nonnull Integer value) implements OneOfWithMatrix {} + + /** + * Creator to enable deserialization of a Integer. + * + * @param val + * the value to use + * @return a new instance of {@link InnerInteger}. + */ + @com.fasterxml.jackson.annotation.JsonCreator + @Nonnull + static InnerInteger create( @Nonnull final Integer val ) + { + return new InnerInteger(val); + } + + /** + * Helper class to create {@code List> } that implements {@link OneOfWithMatrix}. + */ + record InnerIntegers2D(@com.fasterxml.jackson.annotation.JsonValue @Nonnull List> values) implements OneOfWithMatrix {} + + /** + * Creator to enable deserialization of {@code List> }. + * + * @param val + * the value to use + * @return a new instance of {@link InnerIntegers2D}. + */ + @com.fasterxml.jackson.annotation.JsonCreator + @Nonnull + static InnerIntegers2D create2DList( @Nonnull final List> val ) + { + return new InnerIntegers2D(val); + } + +} diff --git a/datamodel/openapi/openapi-api-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/sample/model/OneOfWithMatrixAndArray.java b/datamodel/openapi/openapi-api-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/sample/model/OneOfWithMatrixAndArray.java new file mode 100644 index 000000000..aabbd87f9 --- /dev/null +++ b/datamodel/openapi/openapi-api-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/sample/model/OneOfWithMatrixAndArray.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved. + */ + +/* + * SodaStore API + * API for managing soda products and orders in SodaStore. + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package com.sap.cloud.sdk.datamodel.openapi.sample.model; + +import java.util.List; + +import javax.annotation.Nonnull; + +/** + * OneOfWithMatrixAndArray + */ +public interface OneOfWithMatrixAndArray +{ + /** + * Helper class to create a list of Integer that implements {@link OneOfWithMatrixAndArray}. + */ + record InnerIntegers(@com.fasterxml.jackson.annotation.JsonValue @Nonnull List values) implements OneOfWithMatrixAndArray {} + + /** + * Creator to enable deserialization of a list of Integer. + * + * @param val + * the value to use + * @return a new instance of {@link InnerIntegers}. + */ + @com.fasterxml.jackson.annotation.JsonCreator + @Nonnull + static InnerIntegers create( @Nonnull final List val ) + { + return new InnerIntegers(val); + } + + /** + * Helper class to create {@code List> } that implements {@link OneOfWithMatrixAndArray}. + */ + record InnerIntegers2D(@com.fasterxml.jackson.annotation.JsonValue @Nonnull List> values) implements OneOfWithMatrixAndArray {} + + /** + * Creator to enable deserialization of {@code List> }. + * + * @param val + * the value to use + * @return a new instance of {@link InnerIntegers2D}. + */ + @com.fasterxml.jackson.annotation.JsonCreator + @Nonnull + static InnerIntegers2D create2DList( @Nonnull final List> val ) + { + return new InnerIntegers2D(val); + } + +} diff --git a/datamodel/openapi/openapi-api-sample/src/main/resources/sodastore.yaml b/datamodel/openapi/openapi-api-sample/src/main/resources/sodastore.yaml index ca8212296..71f7d6a68 100644 --- a/datamodel/openapi/openapi-api-sample/src/main/resources/sodastore.yaml +++ b/datamodel/openapi/openapi-api-sample/src/main/resources/sodastore.yaml @@ -117,6 +117,24 @@ components: mapping: disc_foo: '#/components/schemas/Foo' disc_bar: '#/components/schemas/Bar' + OneOfWithMatrix: + oneOf: + - type: integer + - type: array + items: + type: array + items: + type: integer + OneOfWithMatrixAndArray: + oneOf: + - type: array + items: + type: integer + - type: array + items: + type: array + items: + type: integer Foo: type: object properties: diff --git a/datamodel/openapi/openapi-api-sample/src/test/java/com/sap/cloud/sdk/datamodel/openapi/sample/api/OneOfDeserializationTest.java b/datamodel/openapi/openapi-api-sample/src/test/java/com/sap/cloud/sdk/datamodel/openapi/sample/api/OneOfDeserializationTest.java index abd8f1c24..368614a95 100644 --- a/datamodel/openapi/openapi-api-sample/src/test/java/com/sap/cloud/sdk/datamodel/openapi/sample/api/OneOfDeserializationTest.java +++ b/datamodel/openapi/openapi-api-sample/src/test/java/com/sap/cloud/sdk/datamodel/openapi/sample/api/OneOfDeserializationTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.List; import java.util.stream.Stream; import javax.annotation.Nonnull; @@ -17,6 +18,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.sap.cloud.sdk.datamodel.openapi.sample.model.AllOf; import com.sap.cloud.sdk.datamodel.openapi.sample.model.AnyOf; @@ -30,6 +32,8 @@ import com.sap.cloud.sdk.datamodel.openapi.sample.model.OneOfWithDiscriminator; import com.sap.cloud.sdk.datamodel.openapi.sample.model.OneOfWithDiscriminatorAndMapping; import com.sap.cloud.sdk.datamodel.openapi.sample.model.OneOfWithEnumDiscriminator; +import com.sap.cloud.sdk.datamodel.openapi.sample.model.OneOfWithMatrix; +import com.sap.cloud.sdk.datamodel.openapi.sample.model.OneOfWithMatrixAndArray; class OneOfDeserializationTest { @@ -169,6 +173,28 @@ void oneOfWithNestedArrayOfObjects( Class strategy ) } + @Test + void oneOfWIthMatrixOfObjects() + throws JsonProcessingException + { + var json = """ + [ + [1, 2, 3 ], + [4, 5, 6 ] + ] + """; + var matrix = objectMapper.readValue(json, OneOfWithMatrix.class); + assertThat(matrix) + .describedAs("Object should be deserialized as InnerIntegers2D") + .isInstanceOfSatisfying( + OneOfWithMatrix.InnerIntegers2D.class, + integers2D -> assertThat(integers2D.values()).isEqualTo(List.of(List.of(1, 2, 3), List.of(4, 5, 6)))); + + assertThatThrownBy(() -> objectMapper.readValue(json, OneOfWithMatrixAndArray.class)) + .hasMessageContaining("Conflicting array-delegate creators") + .isInstanceOf(InvalidDefinitionException.class); + } + @Test void anyOf() throws JsonProcessingException diff --git a/datamodel/openapi/openapi-generator/src/main/java/com/sap/cloud/sdk/datamodel/openapi/generator/CustomJavaClientCodegen.java b/datamodel/openapi/openapi-generator/src/main/java/com/sap/cloud/sdk/datamodel/openapi/generator/CustomJavaClientCodegen.java index 720e6b47e..4637d4e8a 100644 --- a/datamodel/openapi/openapi-generator/src/main/java/com/sap/cloud/sdk/datamodel/openapi/generator/CustomJavaClientCodegen.java +++ b/datamodel/openapi/openapi-generator/src/main/java/com/sap/cloud/sdk/datamodel/openapi/generator/CustomJavaClientCodegen.java @@ -271,16 +271,30 @@ private void useCreatorsForInterfaceSubtypes( @Nonnull final CodegenModel m ) boolean useCreators = false; for( final Set candidates : List.of(m.anyOf, m.oneOf) ) { int nonPrimitives = 0; - final var candidatesSingle = new HashSet(); - final var candidatesMultiple = new HashSet(); + final var singleTypes = new HashSet(); + final var arrayTypes1D = new HashSet(); + final var arrayTypesND = new HashSet>(); for( final String candidate : candidates ) { if( candidate.startsWith("List<") ) { - final var c1 = candidate.substring(5, candidate.length() - 1); - candidatesMultiple.add(c1); + int depth = 0; + String sub = candidate; + while( sub.startsWith("List<") ) { + sub = sub.substring(5, sub.length() - 1); + depth++; + } + + final String innerType = sub; + if( depth == 1 ) { + arrayTypes1D.add(innerType); + } else { + arrayTypesND + .add(Map.of("innerType", innerType, "depth", String.valueOf(depth), "fullType", candidate)); + } + useCreators = true; } else { - candidatesSingle.add(candidate); + singleTypes.add(candidate); useCreators |= PRIMITIVES.contains(candidate); if( !PRIMITIVES.contains(candidate) ) { nonPrimitives++; @@ -293,8 +307,17 @@ private void useCreatorsForInterfaceSubtypes( @Nonnull final CodegenModel m ) "Generating interface with mixed multiple non-primitive and primitive sub-types: {}. Deserialization may not work."; log.warn(msg, m.name); } + final var numArrayTypes = singleTypes.size() + arrayTypesND.size(); + if( numArrayTypes > 1 ) { + final var msg = + "Field can be oneOf %d array types. Deserialization may not work as expected." + .formatted(numArrayTypes); + log.warn(msg, m.name); + } + candidates.clear(); - final var monads = Map.of("single", candidatesSingle, "multiple", candidatesMultiple); + final var monads = + Map.of("single", singleTypes, "multiple1D", arrayTypes1D, "multipleND", arrayTypesND); m.vendorExtensions.put("x-monads", monads); m.vendorExtensions.put("x-is-one-of-interface", true); // enforce template usage } diff --git a/datamodel/openapi/openapi-generator/src/main/resources/openapi-generator/mustache-templates/oneof_interface.mustache b/datamodel/openapi/openapi-generator/src/main/resources/openapi-generator/mustache-templates/oneof_interface.mustache index b88e6d5a1..e91cae0ae 100644 --- a/datamodel/openapi/openapi-generator/src/main/resources/openapi-generator/mustache-templates/oneof_interface.mustache +++ b/datamodel/openapi/openapi-generator/src/main/resources/openapi-generator/mustache-templates/oneof_interface.mustache @@ -28,7 +28,7 @@ public interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}exte static Inner{{.}} create( @Nonnull final {{.}} val) { return new Inner{{.}}(val); } {{/model.vendorExtensions.x-monads.single}} -{{#model.vendorExtensions.x-monads.multiple}} +{{#model.vendorExtensions.x-monads.multiple1D}} /** * Helper class to create a list of {{.}} that implements {@link {{classname}}}. */ @@ -44,5 +44,22 @@ public interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}exte @Nonnull static Inner{{.}}s create( @Nonnull final List<{{.}}> val) { return new Inner{{.}}s(val); } -{{/model.vendorExtensions.x-monads.multiple}} +{{/model.vendorExtensions.x-monads.multiple1D}} +{{#model.vendorExtensions.x-monads.multipleND}} + /** + * Helper class to create {@code {{{fullType}}} } that implements {@link {{classname}}}. + */ + record Inner{{innerType}}s{{depth}}D(@com.fasterxml.jackson.annotation.JsonValue @Nonnull {{{fullType}}} values) implements {{classname}} {} + + /** + * Creator to enable deserialization of {@code {{{fullType}}} }. + * + * @param val the value to use + * @return a new instance of {@link Inner{{innerType}}s{{depth}}D}. + */ + @com.fasterxml.jackson.annotation.JsonCreator + @Nonnull + static Inner{{innerType}}s{{depth}}D create{{depth}}DList( @Nonnull final {{{fullType}}} val) { return new Inner{{innerType}}s{{depth}}D(val); } + +{{/model.vendorExtensions.x-monads.multipleND}} }