Skip to content

Commit cca5dda

Browse files
wing328thejeff77claude
authored
[kotlin][kotlin-spring] Add companionObject option to generate companion objects in data classes (#23176)
* [kotlin] Add companionObject option to generate companion objects in data classes Add a new boolean CLI option `companionObject` (default: false) that generates an empty `companion object { }` on all data class models, enabling users to add companion extensions from outside the generated code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * [kotlin-spring] Add companionObject option to generate companion objects in data classes Extend the same companionObject option to the kotlin-spring server generator, enabling companion extensions on generated Spring model classes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * update spec, add tests --------- Co-authored-by: Jeffrey Blayney <jeffrey.blayney@missionlane.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5a16826 commit cca5dda

21 files changed

Lines changed: 289 additions & 126 deletions

File tree

bin/configs/kotlin-gson.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
generatorName: kotlin
22
outputDir: samples/client/petstore/kotlin-gson
3-
inputSpec: modules/openapi-generator/src/test/resources/2_0/petstore.yaml
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
44
templateDir: modules/openapi-generator/src/main/resources/kotlin-client
55
additionalProperties:
66
serializationLibrary: gson
77
artifactId: kotlin-petstore-gson
8+
companionObject: true

docs/generators/kotlin-spring.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
2727
|autoXSpringPaginated|Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected.| |false|
2828
|basePackage|base package (invokerPackage) for generated code| |org.openapitools|
2929
|beanQualifiers|Whether to add fully-qualifier class names as bean qualifiers in @Component and @RestController annotations. May be used to prevent bean names clash if multiple generated libraries (contexts) added to single project.| |false|
30+
|companionObject|Whether to generate companion objects in data classes, enabling companion extensions.| |false|
3031
|configPackage|configuration package for generated code| |org.openapitools.configuration|
3132
|declarativeInterfaceReactiveMode|What type of reactive style to use in Spring Http declarative interface|<dl><dt>**coroutines**</dt><dd>Use kotlin-idiomatic 'suspend' functions</dd><dt>**reactor**</dt><dd>Use reactor return wrappers 'Mono' and 'Flux'</dd></dl>|coroutines|
3233
|delegatePattern|Whether to generate the server files using the delegate pattern| |false|

docs/generators/kotlin.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
2323
|artifactId|Generated artifact id (name of jar).| |kotlin-client|
2424
|artifactVersion|Generated artifact's package version.| |1.0.0|
2525
|collectionType|Option. Collection type to use|<dl><dt>**array**</dt><dd>kotlin.Array</dd><dt>**list**</dt><dd>kotlin.collections.List</dd></dl>|list|
26+
|companionObject|Whether to generate companion objects in data classes, enabling companion extensions.| |false|
2627
|dateLibrary|Option. Date library to use|<dl><dt>**threetenbp-localdatetime**</dt><dd>Threetenbp - Backport of JSR310 (jvm only, for legacy app only)</dd><dt>**kotlinx-datetime**</dt><dd>kotlinx-datetime (preferred for multiplatform)</dd><dt>**string**</dt><dd>String</dd><dt>**java8-localdatetime**</dt><dd>Java 8 native JSR310 (jvm only, for legacy app only)</dd><dt>**java8**</dt><dd>Java 8 native JSR310 (jvm only, preferred for jdk 1.8+)</dd><dt>**threetenbp**</dt><dd>Threetenbp - Backport of JSR310 (jvm only, preferred for jdk &lt; 1.8)</dd></dl>|java8|
2728
|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |original|
2829
|explicitApi|Generates code with explicit access modifiers to comply with Kotlin Explicit API Mode.| |false|

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen {
106106

107107
public static final String GENERATE_ONEOF_ANYOF_WRAPPERS = "generateOneOfAnyOfWrappers";
108108

109+
public static final String COMPANION_OBJECT = "companionObject";
110+
109111
protected static final String VENDOR_EXTENSION_BASE_NAME_LITERAL = "x-base-name-literal";
110112

111113

@@ -123,6 +125,7 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen {
123125
@Setter protected boolean mapFileBinaryToByteArray = false;
124126
@Setter protected boolean generateOneOfAnyOfWrappers = true;
125127
@Getter @Setter protected boolean failOnUnknownProperties = false;
128+
@Setter protected boolean companionObject = false;
126129

127130
protected String authFolder;
128131

@@ -288,6 +291,8 @@ public KotlinClientCodegen() {
288291

289292
cliOptions.add(CliOption.newBoolean(GENERATE_ONEOF_ANYOF_WRAPPERS, "Generate oneOf, anyOf schemas as wrappers. Only `jvm-retrofit2`(library) with `gson` or `kotlinx_serialization`(serializationLibrary) support this option."));
290293

294+
cliOptions.add(CliOption.newBoolean(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", false));
295+
291296
CliOption serializationLibraryOpt = new CliOption(CodegenConstants.SERIALIZATION_LIBRARY, SERIALIZATION_LIBRARY_DESC);
292297
cliOptions.add(serializationLibraryOpt.defaultValue(serializationLibrary.name()));
293298

@@ -322,6 +327,10 @@ public boolean getGenerateOneOfAnyOfWrappers() {
322327
return generateOneOfAnyOfWrappers;
323328
}
324329

330+
public boolean getCompanionObject() {
331+
return companionObject;
332+
}
333+
325334
public void setGenerateRoomModels(Boolean generateRoomModels) {
326335
this.generateRoomModels = generateRoomModels;
327336
}
@@ -484,6 +493,12 @@ public void processOpts() {
484493
setFailOnUnknownProperties(false);
485494
}
486495

496+
if (additionalProperties.containsKey(COMPANION_OBJECT)) {
497+
setCompanionObject(convertPropertyToBooleanAndWriteBack(COMPANION_OBJECT));
498+
} else {
499+
additionalProperties.put(COMPANION_OBJECT, companionObject);
500+
}
501+
487502
commonSupportingFiles();
488503

489504
switch (getLibrary()) {
@@ -936,7 +951,7 @@ public ModelsMap postProcessModels(ModelsMap objs) {
936951

937952
for (ModelMap mo : objects.getModels()) {
938953
CodegenModel cm = mo.getModel();
939-
if (getGenerateRoomModels() || getGenerateOneOfAnyOfWrappers()) {
954+
if (getGenerateRoomModels() || getGenerateOneOfAnyOfWrappers() || getCompanionObject()) {
940955
cm.vendorExtensions.put("x-has-data-class-body", true);
941956
}
942957

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
9898
public static final String USE_REQUEST_MAPPING_ON_INTERFACE = "useRequestMappingOnInterface";
9999
public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated";
100100
public static final String USE_SEALED_RESPONSE_INTERFACES = "useSealedResponseInterfaces";
101+
public static final String COMPANION_OBJECT = "companionObject";
101102

102103
@Getter
103104
public enum DeclarativeInterfaceReactiveMode {
@@ -163,6 +164,7 @@ public String getDescription() {
163164
@Setter private boolean useResponseEntity = true;
164165
@Setter private boolean autoXSpringPaginated = false;
165166
@Setter private boolean useSealedResponseInterfaces = false;
167+
@Setter private boolean companionObject = false;
166168

167169
@Getter @Setter
168170
protected boolean useSpringBoot3 = false;
@@ -265,6 +267,7 @@ public KotlinSpringServerCodegen() {
265267
addOption(SCHEMA_IMPLEMENTS, "A map of single interface or a list of interfaces per schema name that should be implemented (serves similar purpose as `x-kotlin-implements`, but is fully decoupled from the api spec). Example: yaml `schemaImplements: {Pet: com.some.pack.WithId, Category: [com.some.pack.CategoryInterface], Dog: [com.some.pack.Canine, com.some.pack.OtherInterface]}` implements interfaces in schemas `Pet` (interface `com.some.pack.WithId`), `Category` (interface `com.some.pack.CategoryInterface`), `Dog`(interfaces `com.some.pack.Canine`, `com.some.pack.OtherInterface`)", "empty map");
266268
addOption(SCHEMA_IMPLEMENTS_FIELDS, "A map of single field or a list of fields per schema name that should be prepended with `override` (serves similar purpose as `x-kotlin-implements-fields`, but is fully decoupled from the api spec). Example: yaml `schemaImplementsFields: {Pet: id, Category: [name, id], Dog: [bark, breed]}` marks fields to be prepended with `override` in schemas `Pet` (field `id`), `Category` (fields `name`, `id`) and `Dog` (fields `bark`, `breed`)", "empty map");
267269
addSwitch(AUTO_X_SPRING_PAGINATED, "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected.", autoXSpringPaginated);
270+
addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject);
268271
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
269272
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
270273
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
@@ -512,6 +515,12 @@ public void processOpts() {
512515
}
513516
writePropertyBack(USE_SEALED_RESPONSE_INTERFACES, useSealedResponseInterfaces);
514517

518+
if (additionalProperties.containsKey(COMPANION_OBJECT)) {
519+
this.setCompanionObject(convertPropertyToBooleanAndWriteBack(COMPANION_OBJECT));
520+
} else {
521+
additionalProperties.put(COMPANION_OBJECT, companionObject);
522+
}
523+
515524
additionalProperties.put("springHttpStatus", new SpringHttpStatusLambda());
516525

517526
// Set basePackage from invokerPackage

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ import {{packageName}}.infrastructure.ITransformForStorage
123123
private const val serialVersionUID: Long = 123
124124
}
125125
{{/serializableModel}}
126-
{{#discriminator}}{{#vars}}{{#required}}
126+
{{^serializableModel}}{{^generateRoomModels}}{{^generateOneOfAnyOfWrappers}}{{#companionObject}} {{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}companion object { }
127+
{{/companionObject}}{{/generateOneOfAnyOfWrappers}}{{/generateRoomModels}}{{/serializableModel}}{{#discriminator}}{{#vars}}{{#required}}
127128
{{>interface_req_var}}{{/required}}{{^required}}
128129
{{>interface_opt_var}}{{/required}}{{/vars}}{{/discriminator}}
129130
{{#hasEnums}}

modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@
6464
private const val serialVersionUID: kotlin.Long = 1
6565
}
6666
{{/serializableModel}}
67-
}
67+
{{^serializableModel}}{{#companionObject}} companion object { }
68+
{{/companionObject}}{{/serializableModel}}}

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

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

4545
import static org.openapitools.codegen.CodegenConstants.*;
46+
import static org.openapitools.codegen.languages.KotlinClientCodegen.COMPANION_OBJECT;
4647
import static org.openapitools.codegen.languages.KotlinClientCodegen.GENERATE_ONEOF_ANYOF_WRAPPERS;
4748

4849
@SuppressWarnings("static-method")
@@ -878,6 +879,50 @@ public void emptyModelKotlinxSerializationTest() throws IOException {
878879
TestUtils.assertFileNotContains(modelKt, "data class EmptyModel");
879880
}
880881

882+
@Test
883+
public void testCompanionObjectAdditionalProperty() {
884+
final KotlinClientCodegen codegen = new KotlinClientCodegen();
885+
886+
// Default case, nothing provided
887+
codegen.processOpts();
888+
889+
ConfigAssert configAssert = new ConfigAssert(codegen.additionalProperties());
890+
// Default to false
891+
configAssert.assertValue(COMPANION_OBJECT, codegen::getCompanionObject, Boolean.FALSE);
892+
893+
// Provide true
894+
codegen.additionalProperties().put(COMPANION_OBJECT, true);
895+
codegen.processOpts();
896+
897+
// Should be true
898+
configAssert.assertValue(COMPANION_OBJECT, codegen::getCompanionObject, Boolean.TRUE);
899+
900+
// Provide false
901+
codegen.additionalProperties().put(COMPANION_OBJECT, false);
902+
codegen.processOpts();
903+
904+
// Should be false
905+
configAssert.assertValue(COMPANION_OBJECT, codegen::getCompanionObject, Boolean.FALSE);
906+
}
907+
908+
@Test
909+
public void testCompanionObjectGeneratesCompanionInModel() throws IOException {
910+
File output = Files.createTempDirectory("test").toFile();
911+
output.deleteOnExit();
912+
913+
final CodegenConfigurator configurator = new CodegenConfigurator()
914+
.setGeneratorName("kotlin")
915+
.addAdditionalProperty(COMPANION_OBJECT, true)
916+
.setInputSpec("src/test/resources/3_0/petstore.yaml")
917+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
918+
919+
DefaultGenerator generator = new DefaultGenerator();
920+
generator.opts(configurator.toClientOptInput()).generate();
921+
922+
Path petModel = Paths.get(output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/client/models/Pet.kt");
923+
TestUtils.assertFileContains(petModel, "companion object { }");
924+
}
925+
881926
private static class ModelNameTest {
882927
private final String expectedName;
883928
private final String expectedClassName;

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4706,6 +4706,35 @@ public void testDeprecatedAnnotationOnController() throws IOException {
47064706
"@Deprecated(message=\"Operation is deprecated\") @RequestMapping("
47074707
);
47084708
}
4709+
4710+
@Test
4711+
public void testCompanionObjectDefaultIsFalse() {
4712+
final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
4713+
codegen.processOpts();
4714+
4715+
Assert.assertEquals(codegen.additionalProperties().get(COMPANION_OBJECT), false);
4716+
}
4717+
4718+
@Test
4719+
public void testCompanionObjectGeneratesCompanionInModel() throws IOException {
4720+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
4721+
output.deleteOnExit();
4722+
String outputPath = output.getAbsolutePath().replace('\\', '/');
4723+
4724+
KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
4725+
codegen.setOutputDir(output.getAbsolutePath());
4726+
codegen.additionalProperties().put(COMPANION_OBJECT, true);
4727+
4728+
new DefaultGenerator().opts(new ClientOptInput()
4729+
.openAPI(TestUtils.parseSpec("src/test/resources/3_0/petstore.yaml"))
4730+
.config(codegen))
4731+
.generate();
4732+
4733+
assertFileContains(
4734+
Paths.get(outputPath + "/src/main/kotlin/org/openapitools/model/Pet.kt"),
4735+
"companion object { }"
4736+
);
4737+
}
47094738
}
47104739

47114740

samples/client/petstore/kotlin-gson/docs/PetApi.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,23 @@ All URIs are relative to *http://petstore.swagger.io/v2*
1616

1717
<a id="addPet"></a>
1818
# **addPet**
19-
> addPet(body)
19+
> Pet addPet(pet)
2020
2121
Add a new pet to the store
2222

23+
24+
2325
### Example
2426
```kotlin
2527
// Import classes:
2628
//import org.openapitools.client.infrastructure.*
2729
//import org.openapitools.client.models.*
2830

2931
val apiInstance = PetApi()
30-
val body : Pet = // Pet | Pet object that needs to be added to the store
32+
val pet : Pet = // Pet | Pet object that needs to be added to the store
3133
try {
32-
apiInstance.addPet(body)
34+
val result : Pet = apiInstance.addPet(pet)
35+
println(result)
3336
} catch (e: ClientException) {
3437
println("4xx response calling PetApi#addPet")
3538
e.printStackTrace()
@@ -42,11 +45,11 @@ try {
4245
### Parameters
4346
| Name | Type | Description | Notes |
4447
| ------------- | ------------- | ------------- | ------------- |
45-
| **body** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | |
48+
| **pet** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | |
4649

4750
### Return type
4851

49-
null (empty response body)
52+
[**Pet**](Pet.md)
5053

5154
### Authorization
5255

@@ -57,14 +60,16 @@ Configure petstore_auth:
5760
### HTTP request headers
5861

5962
- **Content-Type**: application/json
60-
- **Accept**: Not defined
63+
- **Accept**: application/json
6164

6265
<a id="deletePet"></a>
6366
# **deletePet**
6467
> deletePet(petId, apiKey)
6568
6669
Deletes a pet
6770

71+
72+
6873
### Example
6974
```kotlin
7075
// Import classes:
@@ -253,20 +258,23 @@ Configure api_key:
253258

254259
<a id="updatePet"></a>
255260
# **updatePet**
256-
> updatePet(body)
261+
> Pet updatePet(pet)
257262
258263
Update an existing pet
259264

265+
266+
260267
### Example
261268
```kotlin
262269
// Import classes:
263270
//import org.openapitools.client.infrastructure.*
264271
//import org.openapitools.client.models.*
265272

266273
val apiInstance = PetApi()
267-
val body : Pet = // Pet | Pet object that needs to be added to the store
274+
val pet : Pet = // Pet | Pet object that needs to be added to the store
268275
try {
269-
apiInstance.updatePet(body)
276+
val result : Pet = apiInstance.updatePet(pet)
277+
println(result)
270278
} catch (e: ClientException) {
271279
println("4xx response calling PetApi#updatePet")
272280
e.printStackTrace()
@@ -279,11 +287,11 @@ try {
279287
### Parameters
280288
| Name | Type | Description | Notes |
281289
| ------------- | ------------- | ------------- | ------------- |
282-
| **body** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | |
290+
| **pet** | [**Pet**](Pet.md)| Pet object that needs to be added to the store | |
283291

284292
### Return type
285293

286-
null (empty response body)
294+
[**Pet**](Pet.md)
287295

288296
### Authorization
289297

@@ -294,14 +302,16 @@ Configure petstore_auth:
294302
### HTTP request headers
295303

296304
- **Content-Type**: application/json
297-
- **Accept**: Not defined
305+
- **Accept**: application/json
298306

299307
<a id="updatePetWithForm"></a>
300308
# **updatePetWithForm**
301309
> updatePetWithForm(petId, name, status)
302310
303311
Updates a pet in the store with form data
304312

313+
314+
305315
### Example
306316
```kotlin
307317
// Import classes:
@@ -351,6 +361,8 @@ Configure petstore_auth:
351361
352362
uploads an image
353363

364+
365+
354366
### Example
355367
```kotlin
356368
// Import classes:

0 commit comments

Comments
 (0)