Skip to content

feat: [OpenAPI] oneOf with a Tensor (n-Dimensional array) #898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -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<List<Integer>> } that implements {@link OneOfWithMatrix}.
*/
record InnerIntegers2D(@com.fasterxml.jackson.annotation.JsonValue @Nonnull List<List<Integer>> values) implements OneOfWithMatrix {}

/**
* Creator to enable deserialization of {@code List<List<Integer>> }.
*
* @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<List<Integer>> val )
{
return new InnerIntegers2D(val);
}

}
Original file line number Diff line number Diff line change
@@ -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<Integer> 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<Integer> val )
{
return new InnerIntegers(val);
}

/**
* Helper class to create {@code List<List<Integer>> } that implements {@link OneOfWithMatrixAndArray}.
*/
record InnerIntegers2D(@com.fasterxml.jackson.annotation.JsonValue @Nonnull List<List<Integer>> values) implements OneOfWithMatrixAndArray {}

/**
* Creator to enable deserialization of {@code List<List<Integer>> }.
*
* @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<List<Integer>> val )
Copy link
Contributor

@newtork newtork Aug 19, 2025

Choose a reason for hiding this comment

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

(Major/Discussion)

create2DList

I think that's a bad name. Unfortunately I do not have a good alternative at the moment.
I don't think we can use the terms dimension or matrix in our generator, because they indicate well defined m * n vectors, which does not really match the semantics of arrays of arbitrary size having elements of arrays of arbitrary size.

Copy link
Member Author

Choose a reason for hiding this comment

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

To avoid conflicting methods due to type erasure, we need to somehow name the methods distinctly by dimensionality.

Its not clear to me why it is a bad name. And, I don't see how we may name the creators without including the num of dimensions. But, here are few suggestions

  1. from2D, from3D etc
  2. fromDepth2, fromDepth3 etc
  3. fromRank2, fromRank3 etc
  4. create2D, create3D etc
  5. createMatrix, createTensor and cap maximum dimensions supported to 3

No strong opinion on this. So, I will accept any naming you prefer.

{
return new InnerIntegers2D(val);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +120 to +137
Copy link
Contributor

Choose a reason for hiding this comment

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

(Comment)

I think it's a bad idea to abuse the sample module das unit test ground. We should move it to generator module instead.
However since probably I'm offender too, I cannot force it here.
Maybe as part of a separate house-keeping PR.

Foo:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,16 +271,30 @@ private void useCreatorsForInterfaceSubtypes( @Nonnull final CodegenModel m )
boolean useCreators = false;
for( final Set<String> candidates : List.of(m.anyOf, m.oneOf) ) {
int nonPrimitives = 0;
final var candidatesSingle = new HashSet<String>();
final var candidatesMultiple = new HashSet<String>();
final var singleTypes = new HashSet<String>();
final var arrayTypes1D = new HashSet<String>();
final var arrayTypesND = new HashSet<Map<String, String>>();
Comment on lines +274 to +276
Copy link
Contributor

Choose a reason for hiding this comment

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

(Minor)

  1. Can you combine arrayTypes1D and arrayTypesND? I don't see a reason why we need to maintain both sets(?)
  2. Have you considered a record type instead of generic Map<String, String>? Looks a little lazy for storing 3 static values.


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++;
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}}.
*/
Expand All @@ -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}}
}