Skip to content

Commit e86daf9

Browse files
authored
Add support for oneOf with discriminator when using kotlinx.serialization (#22373)
* Generate wrappers for oneOf with discriminator when using kotlinx.serialization * Add spec with oneOf using discriminator * Add config to generate samples * Generate samples * Update samples * Change naming of wrapper classes * Fix empty model test * Update GH workflow with new samples
1 parent c52cc1f commit e86daf9

File tree

51 files changed

+1949
-19
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1949
-19
lines changed

.github/workflows/samples-kotlin-client.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ jobs:
7272
- samples/client/others/kotlin-jvm-okhttp-path-comments
7373
- samples/client/others/kotlin-integer-enum
7474
- samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization
75+
- samples/client/others/kotlin-oneOf-discriminator-kotlinx-serialization
7576
steps:
7677
- uses: actions/checkout@v5
7778
- uses: actions/setup-java@v5
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
generatorName: kotlin
2+
outputDir: samples/client/others/kotlin-oneOf-discriminator-kotlinx-serialization
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism-oneof-discriminator.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/kotlin-client
5+
additionalProperties:
6+
artifactId: kotlin-oneOf-discriminator
7+
serializableModel: "false"
8+
dateLibrary: java8
9+
library: jvm-retrofit2
10+
enumUnknownDefaultCase: true
11+
serializationLibrary: kotlinx_serialization
12+
generateOneOfAnyOfWrappers: true

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.fasterxml.jackson.databind.node.ArrayNode;
2121
import com.google.common.collect.ImmutableMap;
2222
import com.samskivert.mustache.Mustache;
23+
import com.samskivert.mustache.Template;
2324
import io.swagger.v3.oas.models.media.Schema;
2425
import io.swagger.v3.oas.models.media.StringSchema;
2526
import lombok.Getter;
@@ -35,6 +36,9 @@
3536
import org.slf4j.LoggerFactory;
3637

3738
import java.io.File;
39+
import java.io.IOException;
40+
import java.io.StringWriter;
41+
import java.io.Writer;
3842
import java.util.*;
3943
import java.util.function.Function;
4044
import java.util.regex.Pattern;
@@ -1172,4 +1176,15 @@ protected void doDataTypeAssignment(final String returnType, DataTypeAssigner da
11721176
}
11731177
}
11741178
}
1179+
1180+
protected static abstract class CustomLambda implements Mustache.Lambda {
1181+
@Override
1182+
public void execute(Template.Fragment frag, Writer out) throws IOException {
1183+
final StringWriter tempWriter = new StringWriter();
1184+
frag.execute(tempWriter);
1185+
out.write(formatFragment(tempWriter.toString()));
1186+
}
1187+
1188+
public abstract String formatFragment(String fragment);
1189+
}
11751190
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import lombok.Setter;
2323
import org.apache.commons.lang3.StringUtils;
2424
import org.openapitools.codegen.*;
25-
import org.openapitools.codegen.meta.features.*;
2625
import org.openapitools.codegen.model.ModelMap;
2726
import org.openapitools.codegen.model.ModelsMap;
2827
import org.openapitools.codegen.model.OperationMap;
@@ -40,10 +39,8 @@
4039
import java.util.stream.Collectors;
4140
import java.util.stream.Stream;
4241

43-
import com.samskivert.mustache.Mustache;
4442
import lombok.Getter;
4543
import lombok.Setter;
46-
import org.apache.commons.lang3.StringUtils;
4744
import org.openapitools.codegen.CliOption;
4845
import org.openapitools.codegen.CodegenConstants;
4946
import org.openapitools.codegen.CodegenModel;
@@ -60,14 +57,7 @@
6057
import org.openapitools.codegen.meta.features.SchemaSupportFeature;
6158
import org.openapitools.codegen.meta.features.SecurityFeature;
6259
import org.openapitools.codegen.meta.features.WireFormatFeature;
63-
import org.openapitools.codegen.model.ModelMap;
64-
import org.openapitools.codegen.model.ModelsMap;
65-
import org.openapitools.codegen.model.OperationMap;
66-
import org.openapitools.codegen.model.OperationsMap;
6760
import org.openapitools.codegen.templating.mustache.ReplaceAllLambda;
68-
import org.openapitools.codegen.utils.ProcessUtils;
69-
import org.slf4j.Logger;
70-
import org.slf4j.LoggerFactory;
7161

7262
import static java.util.Collections.sort;
7363

@@ -569,6 +559,7 @@ public void processOpts() {
569559
// as the parser interrupts that as a start of a multiline comment.
570560
// We replace paths like `/v1/foo/*` with `/v1/foo/<*>` to avoid this
571561
additionalProperties.put("sanitizePathComment", new ReplaceAllLambda("\\/\\*", "/<*>"));
562+
additionalProperties.put("fnToOneOfWrapperName", new ToOneOfWrapperName());
572563
}
573564

574565
private void processDateLibrary() {
@@ -974,11 +965,21 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
974965
if (discriminator == null) {
975966
continue;
976967
}
968+
969+
// When using generateOneOfAnyOfWrappers and encountering oneOf, we keep discriminator properties,
970+
// because single entity can be referenced in multiple "parent" entities,
971+
// so discriminator for one might not be discriminator for another.
972+
boolean shouldKeepDiscriminatorField = generateOneOfAnyOfWrappers && cm.oneOf != null && !cm.oneOf.isEmpty();
973+
974+
if (shouldKeepDiscriminatorField) {
975+
continue;
976+
}
977+
977978
// Remove discriminator property from the base class, it is not needed in the generated code
978979
getAllVarProperties(cm).forEach(list -> list.removeIf(var -> var.name.equals(discriminator.getPropertyName())));
979980

980981
for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
981-
// Add the mapping name to additionalProperties.disciminatorValue
982+
// Add the mapping name to additionalProperties.discriminatorValue
982983
// The mapping name is used to define SerializedName, which in result makes derived classes
983984
// found by kotlinx-serialization during deserialization
984985
CodegenProperty additionalProperties = mappedModel.getModel().getAdditionalProperties();
@@ -996,7 +997,6 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
996997
mappedModel.getModel().setHasVars(false);
997998
}
998999
}
999-
10001000
}
10011001
}
10021002
}
@@ -1141,6 +1141,13 @@ private void adjustEnumRefDefault(CodegenParameter param) {
11411141
param.defaultValue = type + "." + param.enumDefaultValue;
11421142
}
11431143

1144+
private class ToOneOfWrapperName extends CustomLambda {
1145+
@Override
1146+
public String formatFragment(String fragment) {
1147+
return toModelName(StringUtils.lowerCase(fragment)) + "Wrapper";
1148+
}
1149+
}
1150+
11441151
@Override
11451152
public void postProcess() {
11461153
System.out.println("################################################################################");

modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,28 @@ import kotlinx.serialization.builtins.serializer
3737
import kotlinx.serialization.encoding.Decoder
3838
import kotlinx.serialization.encoding.Encoder
3939
{{/enumUnknownDefaultCase}}
40+
{{^enumUnknownDefaultCase}}
41+
{{#generateOneOfAnyOfWrappers}}
42+
{{#discriminator}}
43+
import kotlinx.serialization.KSerializer
44+
import kotlinx.serialization.encoding.Decoder
45+
import kotlinx.serialization.encoding.Encoder
46+
{{/discriminator}}
47+
{{/generateOneOfAnyOfWrappers}}
48+
{{/enumUnknownDefaultCase}}
49+
{{#generateOneOfAnyOfWrappers}}
50+
{{#discriminator}}
51+
import kotlinx.serialization.SerializationException
52+
import kotlinx.serialization.descriptors.SerialDescriptor
53+
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
54+
import kotlinx.serialization.json.JsonDecoder
55+
import kotlinx.serialization.json.JsonEncoder
56+
import kotlinx.serialization.json.JsonObject
57+
import kotlinx.serialization.json.JsonPrimitive
58+
import kotlinx.serialization.json.jsonObject
59+
import kotlinx.serialization.json.jsonPrimitive
60+
{{/discriminator}}
61+
{{/generateOneOfAnyOfWrappers}}
4062
{{#hasEnums}}
4163
{{/hasEnums}}
4264
{{/kotlinx_serialization}}
@@ -57,7 +79,9 @@ import java.io.Serializable
5779
import {{roomModelPackage}}.{{classname}}RoomModel
5880
import {{packageName}}.infrastructure.ITransformForStorage
5981
{{/generateRoomModels}}
82+
{{^kotlinx_serialization}}
6083
import java.io.IOException
84+
{{/kotlinx_serialization}}
6185

6286
/**
6387
* {{{description}}}
@@ -66,12 +90,68 @@ import java.io.IOException
6690
{{#parcelizeModels}}
6791
@Parcelize
6892
{{/parcelizeModels}}
93+
{{^generateOneOfAnyOfWrappers}}
94+
{{^discriminator}}
6995
{{#multiplatform}}{{^discriminator}}@Serializable{{/discriminator}}{{/multiplatform}}{{#kotlinx_serialization}}{{#serializableModel}}@KSerializable{{/serializableModel}}{{^serializableModel}}@Serializable{{/serializableModel}}{{/kotlinx_serialization}}{{#moshi}}{{#moshiCodeGen}}@JsonClass(generateAdapter = true){{/moshiCodeGen}}{{/moshi}}{{#jackson}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{/jackson}}
96+
{{/discriminator}}
97+
{{/generateOneOfAnyOfWrappers}}
98+
{{#kotlinx_serialization}}
99+
{{#generateOneOfAnyOfWrappers}}
100+
{{#discriminator}}
101+
@Serializable(with = {{classname}}Serializer::class)
102+
{{/discriminator}}
103+
{{/generateOneOfAnyOfWrappers}}
104+
{{/kotlinx_serialization}}
70105
{{#isDeprecated}}
71106
@Deprecated(message = "This schema is deprecated.")
72107
{{/isDeprecated}}
73108
{{>additionalModelTypeAnnotations}}
109+
{{#kotlinx_serialization}}
110+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}sealed interface {{classname}} {
111+
{{#discriminator.mappedModels}}
112+
@JvmInline
113+
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}} value class {{#fnToOneOfWrapperName}}{{mappingName}}{{/fnToOneOfWrapperName}}(val value: {{modelName}}) : {{classname}}
74114

115+
{{/discriminator.mappedModels}}
116+
}
117+
118+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object {{classname}}Serializer : KSerializer<{{classname}}> {
119+
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("{{classname}}")
120+
121+
override fun serialize(encoder: Encoder, value: {{classname}}) {
122+
require(encoder is JsonEncoder)
123+
val jsonObject = when (value) {
124+
{{#discriminator.mappedModels}}
125+
is {{classname}}.{{#fnToOneOfWrapperName}}{{mappingName}}{{/fnToOneOfWrapperName}} -> {
126+
val jsonMap = encoder.json.encodeToJsonElement({{modelName}}.serializer(), value.value).jsonObject.toMutableMap()
127+
jsonMap["{{discriminator.propertyBaseName}}"] = JsonPrimitive("{{mappingName}}")
128+
JsonObject(jsonMap)
129+
}
130+
{{/discriminator.mappedModels}}
131+
}
132+
encoder.encodeJsonElement(jsonObject)
133+
}
134+
135+
override fun deserialize(decoder: Decoder): {{classname}} {
136+
require(decoder is JsonDecoder)
137+
val element = decoder.decodeJsonElement().jsonObject
138+
139+
val discriminatorValue = element["{{discriminator.propertyBaseName}}"]?.jsonPrimitive?.content
140+
?: throw SerializationException("Missing {{discriminator.propertyBaseName}} field")
141+
142+
return when (discriminatorValue) {
143+
{{#discriminator.mappedModels}}
144+
"{{mappingName}}" -> {
145+
val decoded = decoder.json.decodeFromJsonElement({{modelName}}.serializer(), element)
146+
{{classname}}.{{#fnToOneOfWrapperName}}{{mappingName}}{{/fnToOneOfWrapperName}}(decoded)
147+
}
148+
{{/discriminator.mappedModels}}
149+
else -> throw SerializationException("Unknown {{classname}} {{discriminator.propertyBaseName}}: $discriminatorValue")
150+
}
151+
}
152+
}
153+
{{/kotlinx_serialization}}
154+
{{^kotlinx_serialization}}
75155
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {
76156
77157
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class CustomTypeAdapterFactory : TypeAdapterFactory {
@@ -332,4 +412,5 @@ import java.io.IOException
332412
}
333413
}
334414
}
335-
}
415+
}
416+
{{/kotlinx_serialization}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
import java.util.Map;
4444

4545
import static org.openapitools.codegen.CodegenConstants.*;
46-
import static org.openapitools.codegen.TestUtils.assertFileContains;
46+
import static org.openapitools.codegen.languages.KotlinClientCodegen.GENERATE_ONEOF_ANYOF_WRAPPERS;
4747

4848
@SuppressWarnings("static-method")
4949
public class KotlinClientCodegenModelTest {
@@ -502,7 +502,7 @@ private void givenSchemaObjectPropertyNameContainsDollarSignWhenGenerateThenDoll
502502
// properties.put(CodegenConstants.LIBRARY, ClientLibrary.JVM_KTOR);
503503
properties.put(CodegenConstants.ENUM_PROPERTY_NAMING, CodegenConstants.ENUM_PROPERTY_NAMING_TYPE.UPPERCASE.toString());
504504
properties.put(SERIALIZATION_LIBRARY, KotlinClientCodegen.SERIALIZATION_LIBRARY_TYPE.gson.toString());
505-
properties.put(KotlinClientCodegen.GENERATE_ONEOF_ANYOF_WRAPPERS, true);
505+
properties.put(GENERATE_ONEOF_ANYOF_WRAPPERS, true);
506506
properties.put(API_PACKAGE, "com.toasttab.service.scim.api");
507507
properties.put(MODEL_PACKAGE, "com.toasttab.service.scim.models");
508508
properties.put(PACKAGE_NAME, "com.toasttab.service.scim");
@@ -661,6 +661,7 @@ public void emptyModelKotlinxSerializationTest() throws IOException {
661661
.setGeneratorName("kotlin")
662662
.setAdditionalProperties(new HashMap<>() {{
663663
put(CodegenConstants.MODEL_PACKAGE, "model");
664+
put(GENERATE_ONEOF_ANYOF_WRAPPERS, false);
664665
put(SERIALIZATION_LIBRARY, "kotlinx_serialization");
665666
}})
666667
.setInputSpec("src/test/resources/3_0/kotlin/empty-model.yaml")
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
openapi: 3.0.1
2+
info:
3+
title: Example
4+
description: An example
5+
version: '0.1'
6+
contact:
7+
8+
url: 'https://example.org'
9+
servers:
10+
- url: http://example.org
11+
tags:
12+
- name: bird
13+
paths:
14+
'/v1/bird/{id}':
15+
get:
16+
tags:
17+
- bird
18+
responses:
19+
'200':
20+
description: OK
21+
content:
22+
application/json:
23+
schema:
24+
$ref: '#/components/schemas/animal'
25+
operationId: get-bird
26+
parameters:
27+
- schema:
28+
type: string
29+
format: uuid
30+
name: id
31+
in: path
32+
required: true
33+
components:
34+
schemas:
35+
animal:
36+
title: An animal
37+
oneOf:
38+
- $ref: '#/components/schemas/bird'
39+
- $ref: '#/components/schemas/robobird'
40+
discriminator:
41+
propertyName: discriminator
42+
mapping:
43+
BIRD: '#/components/schemas/bird'
44+
ROBOBIRD: '#/components/schemas/robobird'
45+
another_animal:
46+
title: Another animal
47+
oneOf:
48+
- $ref: '#/components/schemas/bird'
49+
- $ref: '#/components/schemas/robobird'
50+
discriminator:
51+
propertyName: another_discriminator
52+
mapping:
53+
ANOTHER_BIRD: '#/components/schemas/bird'
54+
ANOTHER_ROBOBIRD: '#/components/schemas/robobird'
55+
56+
bird:
57+
title: A bird
58+
required:
59+
- discriminator
60+
- another_discriminator
61+
properties:
62+
propertyA:
63+
type: string
64+
sameNameProperty:
65+
type: integer
66+
discriminator:
67+
type: string
68+
another_discriminator:
69+
type: string
70+
robobird:
71+
title: A robo-bird
72+
required:
73+
- discriminator
74+
- another_discriminator
75+
properties:
76+
propertyB:
77+
type: string
78+
sameNameProperty:
79+
type: string
80+
discriminator:
81+
type: string
82+
another_discriminator:
83+
type: string
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# OpenAPI Generator Ignore
2+
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
3+
4+
# Use this file to prevent files from being overwritten by the generator.
5+
# The patterns follow closely to .gitignore or .dockerignore.
6+
7+
# As an example, the C# client generator defines ApiClient.cs.
8+
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
9+
#ApiClient.cs
10+
11+
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
12+
#foo/*/qux
13+
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
14+
15+
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
16+
#foo/**/qux
17+
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
18+
19+
# You can also negate patterns with an exclamation (!).
20+
# For example, you can ignore all files in a docs folder with the file extension .md:
21+
#docs/*.md
22+
# Then explicitly reverse the ignore rule for a single file:
23+
#!docs/README.md

0 commit comments

Comments
 (0)