Skip to content

Commit 67ecf24

Browse files
authored
feat: [OpenAPI] Improved OneOf Support (#657)
1 parent ae36154 commit 67ecf24

File tree

18 files changed

+146
-68
lines changed

18 files changed

+146
-68
lines changed

datamodel/openapi/openapi-api-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/sample/model/OneOf.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616

1717
package com.sap.cloud.sdk.datamodel.openapi.sample.model;
1818

19+
import com.fasterxml.jackson.annotation.JsonSubTypes;
1920
import com.fasterxml.jackson.annotation.JsonTypeInfo;
2021

2122
/**
2223
* OneOf
2324
*/
24-
25-
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "", visible = true )
25+
@JsonTypeInfo( use = JsonTypeInfo.Id.DEDUCTION )
26+
@JsonSubTypes( { @JsonSubTypes.Type( value = Cola.class ), @JsonSubTypes.Type( value = Fanta.class ), } )
2627

2728
public interface OneOf
2829
{

datamodel/openapi/openapi-api-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/sample/model/OneOfWithDiscriminator.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@
2222
/**
2323
* OneOfWithDiscriminator
2424
*/
25-
26-
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "sodaType", visible = true )
25+
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "sodaType", visible = true )
2726
@JsonSubTypes( {
2827
@JsonSubTypes.Type( value = Cola.class, name = "Cola" ),
2928
@JsonSubTypes.Type( value = Fanta.class, name = "Fanta" ), } )
3029

3130
public interface OneOfWithDiscriminator
3231
{
33-
public String getSodaType();
32+
String getSodaType();
3433
}

datamodel/openapi/openapi-api-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/sample/model/OneOfWithDiscriminatorAndMapping.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222
/**
2323
* OneOfWithDiscriminatorAndMapping
2424
*/
25-
26-
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "sodaType", visible = true )
25+
@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "sodaType", visible = true )
2726
@JsonSubTypes( {
27+
@JsonSubTypes.Type( value = Cola.class, name = "cool_cola" ),
28+
@JsonSubTypes.Type( value = Fanta.class, name = "fancy_fanta" ),
2829
@JsonSubTypes.Type( value = Cola.class, name = "Cola" ),
2930
@JsonSubTypes.Type( value = Fanta.class, name = "Fanta" ), } )
3031

3132
public interface OneOfWithDiscriminatorAndMapping
3233
{
33-
public String getSodaType();
34+
String getSodaType();
3435
}

datamodel/openapi/openapi-api-sample/src/main/resources/sodastore.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ components:
9999
discriminator:
100100
propertyName: sodaType
101101
mapping:
102-
Cola: '#/components/schemas/Cola'
103-
Fanta: '#/components/schemas/Fanta'
102+
cool_cola: '#/components/schemas/Cola'
103+
fancy_fanta: '#/components/schemas/Fanta'
104104
OneOfWithDiscriminator:
105105
oneOf:
106106
- $ref: '#/components/schemas/Cola'

datamodel/openapi/openapi-api-sample/src/test/java/com/sap/cloud/sdk/datamodel/openapi/sample/api/OneOfDeserializationTest.java

Lines changed: 95 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.sap.cloud.sdk.datamodel.openapi.sample.api;
22

3+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
34
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
4-
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
55

66
import javax.annotation.Nonnull;
77

@@ -11,8 +11,8 @@
1111
import com.fasterxml.jackson.annotation.JsonAutoDetect;
1212
import com.fasterxml.jackson.annotation.PropertyAccessor;
1313
import com.fasterxml.jackson.core.JsonProcessingException;
14+
import com.fasterxml.jackson.databind.JsonNode;
1415
import com.fasterxml.jackson.databind.ObjectMapper;
15-
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
1616
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
1717
import com.sap.cloud.sdk.datamodel.openapi.sample.model.AllOf;
1818
import com.sap.cloud.sdk.datamodel.openapi.sample.model.AnyOf;
@@ -22,86 +22,152 @@
2222
import com.sap.cloud.sdk.datamodel.openapi.sample.model.OneOfWithDiscriminator;
2323
import com.sap.cloud.sdk.datamodel.openapi.sample.model.OneOfWithDiscriminatorAndMapping;
2424

25-
public class OneOfDeserializationTest
25+
class OneOfDeserializationTest
2626
{
27-
String cola = """
27+
private static final ObjectMapper objectMapper = newDefaultObjectMapper();
28+
29+
private static final Cola COLA_OBJECT = Cola.create().caffeine(true).sodaType("Cola");
30+
private static final Fanta FANTA_OBJECT = Fanta.create().color("orange").sodaType("Fanta");
31+
private static final String COLA_JSON = """
2832
{
2933
"sodaType": "Cola",
3034
"caffeine": true
3135
}""";
32-
String fanta = """
36+
private static final String FANTA_JSON = """
3337
{
3438
"sodaType": "Fanta",
3539
"color": "orange"
3640
}""";
41+
private static final String UNKNOWN_JSON = """
42+
{
43+
"sodaType": "Sprite",
44+
"someProperty": "someValue"
45+
}""";
3746

3847
@Test
3948
void oneOf()
49+
throws JsonProcessingException
4050
{
41-
// useOneOfInterfaces is enabled and no discriminator is present, the deserialization will fail
42-
// The fix is to use set a mixIn in the ObjectMapper
43-
assertThatThrownBy(() -> newDefaultObjectMapper().readValue(cola, OneOf.class))
44-
.isInstanceOf(InvalidTypeIdException.class)
45-
.hasMessageContaining("Could not resolve subtype");
46-
assertThatThrownBy(() -> newDefaultObjectMapper().readValue(fanta, OneOf.class))
47-
.isInstanceOf(InvalidTypeIdException.class)
48-
.hasMessageContaining("Could not resolve subtype");
51+
var actual = objectMapper.readValue(COLA_JSON, OneOf.class);
52+
assertThat(actual)
53+
.describedAs("Object should automatically be deserialized as Cola with JSON subtype deduction")
54+
.isInstanceOf(Cola.class)
55+
.isEqualTo(COLA_OBJECT);
56+
57+
actual = objectMapper.readValue(FANTA_JSON, OneOf.class);
58+
assertThat(actual)
59+
.describedAs("Object should automatically be deserialized as Fanta with JSON subtype deduction")
60+
.isInstanceOf(Fanta.class)
61+
.isEqualTo(FANTA_OBJECT);
62+
63+
assertThatThrownBy(() -> objectMapper.readValue(UNKNOWN_JSON, OneOf.class))
64+
.isInstanceOf(JsonProcessingException.class);
4965
}
5066

5167
@Test
5268
void oneOfWithDiscriminator()
5369
throws JsonProcessingException
5470
{
55-
Cola oneOfCola = (Cola) newDefaultObjectMapper().readValue(cola, OneOfWithDiscriminator.class);
56-
assertThat(oneOfCola.getSodaType()).isEqualTo("Cola");
57-
assertThat(oneOfCola.isCaffeine()).isTrue();
71+
var actual = objectMapper.readValue(COLA_JSON, OneOfWithDiscriminator.class);
72+
assertThat(actual)
73+
.describedAs(
74+
"Object should automatically be deserialized as Cola using the class names as discriminator mapping values")
75+
.isInstanceOf(Cola.class)
76+
.isEqualTo(COLA_OBJECT);
5877

59-
Fanta oneOfFanta = (Fanta) newDefaultObjectMapper().readValue(fanta, OneOfWithDiscriminator.class);
60-
assertThat(oneOfFanta.getSodaType()).isEqualTo("Fanta");
61-
assertThat(oneOfFanta.getColor()).isEqualTo("orange");
78+
actual = objectMapper.readValue(FANTA_JSON, OneOfWithDiscriminator.class);
79+
assertThat(actual)
80+
.describedAs(
81+
"Object should automatically be deserialized as Fanta using the class names as discriminator mapping values")
82+
.isInstanceOf(Fanta.class)
83+
.isEqualTo(FANTA_OBJECT);
84+
85+
assertThatThrownBy(() -> objectMapper.readValue(UNKNOWN_JSON, OneOfWithDiscriminator.class));
6286
}
6387

6488
@Test
6589
void oneOfWithDiscriminatorAndMapping()
6690
throws JsonProcessingException
6791
{
68-
Cola oneOfCola = (Cola) newDefaultObjectMapper().readValue(cola, OneOfWithDiscriminatorAndMapping.class);
69-
assertThat(oneOfCola.getSodaType()).isEqualTo("Cola");
70-
assertThat(oneOfCola.isCaffeine()).isTrue();
92+
var jsonWithCustomMapping = """
93+
{
94+
"sodaType": "cool_cola",
95+
"caffeine": true
96+
}""";
97+
var actual = objectMapper.readValue(jsonWithCustomMapping, OneOfWithDiscriminatorAndMapping.class);
98+
assertThat(actual)
99+
.describedAs(
100+
"Object should automatically be deserialized as Cola using the explicit discriminator mapping values")
101+
.isInstanceOf(Cola.class)
102+
.isEqualTo(Cola.create().caffeine(true).sodaType("cool_cola"));
103+
104+
jsonWithCustomMapping = """
105+
{
106+
"sodaType": "fancy_fanta",
107+
"color": "orange"
108+
}""";
109+
actual = objectMapper.readValue(jsonWithCustomMapping, OneOfWithDiscriminatorAndMapping.class);
110+
assertThat(actual)
111+
.describedAs(
112+
"Object should automatically be deserialized as Fanta using the explicit discriminator mapping values")
113+
.isInstanceOf(Fanta.class)
114+
.isEqualTo(Fanta.create().color("orange").sodaType("fancy_fanta"));
115+
116+
assertThatThrownBy(() -> objectMapper.readValue(UNKNOWN_JSON, OneOfWithDiscriminatorAndMapping.class))
117+
.isInstanceOf(JsonProcessingException.class);
71118

72-
Fanta oneOfFanta = (Fanta) newDefaultObjectMapper().readValue(fanta, OneOfWithDiscriminatorAndMapping.class);
73-
assertThat(oneOfFanta.getSodaType()).isEqualTo("Fanta");
74-
assertThat(oneOfFanta.getColor()).isEqualTo("orange");
75119
}
76120

77121
@Test
78122
void anyOf()
79123
throws JsonProcessingException
80124
{
81-
AnyOf anyOfCola = newDefaultObjectMapper().readValue(cola, AnyOf.class);
125+
AnyOf anyOfCola = objectMapper.readValue(COLA_JSON, AnyOf.class);
82126
assertThat(anyOfCola.getSodaType()).isEqualTo("Cola");
83127
assertThat(anyOfCola.isCaffeine()).isTrue();
128+
assertThat(anyOfCola.getColor()).isNull();
84129

85-
AnyOf anyOfFanta = newDefaultObjectMapper().readValue(fanta, AnyOf.class);
130+
AnyOf anyOfFanta = objectMapper.readValue(FANTA_JSON, AnyOf.class);
86131
assertThat(anyOfFanta.getSodaType()).isEqualTo("Fanta");
87132
assertThat(anyOfFanta.getColor()).isEqualTo("orange");
133+
assertThat(anyOfFanta.isCaffeine()).isNull();
88134
}
89135

90136
@Test
91137
void allOf()
92138
throws JsonProcessingException
93139
{
94-
AllOf allOfCola = newDefaultObjectMapper().readValue(cola, AllOf.class);
140+
AllOf allOfCola = objectMapper.readValue(COLA_JSON, AllOf.class);
95141
assertThat(allOfCola.getSodaType()).isEqualTo("Cola");
96142
assertThat(allOfCola.isCaffeine()).isTrue();
97143
assertThat(allOfCola.getColor()).isNull();
98144

99-
AllOf allOfFanta = newDefaultObjectMapper().readValue(fanta, AllOf.class);
145+
AllOf allOfFanta = objectMapper.readValue(FANTA_JSON, AllOf.class);
100146
assertThat(allOfFanta.getSodaType()).isEqualTo("Fanta");
101147
assertThat(allOfFanta.getColor()).isEqualTo("orange");
102148
assertThat(allOfFanta.isCaffeine()).isNull();
103149
}
104150

151+
@Test
152+
void testColaSerialization()
153+
throws JsonProcessingException
154+
{
155+
var expected = objectMapper.readValue(COLA_JSON, JsonNode.class);
156+
var actual = objectMapper.valueToTree(expected);
157+
158+
assertThat(actual).isEqualTo(expected);
159+
}
160+
161+
@Test
162+
void testFantaSerialization()
163+
throws JsonProcessingException
164+
{
165+
var expected = objectMapper.readValue(FANTA_JSON, JsonNode.class);
166+
var actual = objectMapper.valueToTree(expected);
167+
168+
assertThat(actual).isEqualTo(expected);
169+
}
170+
105171
/**
106172
* Taken from {@link com.sap.cloud.sdk.services.openapi.apiclient.ApiClient}
107173
*/

datamodel/openapi/openapi-generator/src/main/java/com/sap/cloud/sdk/datamodel/openapi/generator/GenerationConfigurationConverter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ private static void setGlobalSettings( @Nonnull final GenerationConfiguration co
104104
if( configuration.isGenerateModels() ) {
105105
GlobalSettings.setProperty(CodegenConstants.MODELS, "");
106106
}
107+
if( configuration.isDebugModels() ) {
108+
GlobalSettings.setProperty("debugModels", "true");
109+
}
107110
GlobalSettings.setProperty(CodegenConstants.MODEL_TESTS, Boolean.FALSE.toString());
108111
GlobalSettings.setProperty(CodegenConstants.MODEL_DOCS, Boolean.FALSE.toString());
109112
GlobalSettings.setProperty(CodegenConstants.API_TESTS, Boolean.FALSE.toString());

datamodel/openapi/openapi-generator/src/main/java/com/sap/cloud/sdk/datamodel/openapi/generator/model/GenerationConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ public class GenerationConfiguration
6262
@Builder.Default
6363
boolean generateApis = true;
6464

65+
@Builder.Default
66+
boolean debugModels = false;
67+
6568
/**
6669
* Indicates whether to use the default SAP copyright header for generated files.
6770
*

datamodel/openapi/openapi-generator/src/main/resources/openapi-generator/mustache-templates/oneof_interface.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
{{>additionalOneOfTypeAnnotations}}{{>typeInfoAnnotation}}{{>xmlAnnotation}}
77
public interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
88
{{#discriminator}}
9-
public {{propertyType}} {{propertyGetter}}();
9+
{{propertyType}} {{propertyGetter}}();
1010
{{/discriminator}}
1111
}
Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
{{#jackson}}
2-
3-
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true)
4-
{{#discriminator.mappedModels}}
5-
{{#-first}}
2+
{{#discriminator}}
3+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "{{{discriminator.propertyBaseName}}}", visible = true)
4+
{{/discriminator}}
5+
{{^discriminator}}
6+
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
7+
{{/discriminator}}
68
@JsonSubTypes({
7-
{{/-first}}
8-
@JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"),
9-
{{#-last}}
10-
})
11-
{{/-last}}
9+
{{#discriminator.mappedModels}}
10+
@JsonSubTypes.Type(value = {{modelName}}.class{{#discriminator}}, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"{{/discriminator}}),
1211
{{/discriminator.mappedModels}}
13-
{{/jackson}}
12+
{{^discriminator.mappedModels}}
13+
{{#model.oneOf}}
14+
@JsonSubTypes.Type(value = {{.}}.class),
15+
{{/model.oneOf}}
16+
{{/discriminator.mappedModels}}
17+
})
18+
{{/jackson}}

datamodel/openapi/openapi-generator/src/test/java/com/sap/cloud/sdk/datamodel/openapi/generator/DataModelGeneratorIntegrationTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ void integrationTests( final TestCase testCase, @TempDir final Path path )
163163
final var generationConfiguration =
164164
GenerationConfiguration
165165
.builder()
166+
// .debugModels(true) enable this for better mustache file debugging
166167
.apiPackage(testCase.apiPackageName)
167168
.generateApis(testCase.generateApis)
168169
.modelPackage(testCase.modelPackageName)

0 commit comments

Comments
 (0)