Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/generators/kotlin-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
|implicitHeaders|Skip header parameters in the generated API methods.| |false|
|interfaceOnly|Whether to generate only API interface stubs without the server files. This option is currently supported only when using jaxrs-spec library.| |false|
|inheritanceMode|Strategy for model inheritance generation. Values: `none`, `sealed`, `abstract`, `composition`.|<dl><dt>**none**</dt><dd>Flatten inheritance metadata (no discriminator/parent shaping).</dd><dt>**sealed**</dt><dd>Use sealed/interface-oriented shaping (supported for `ktor`, `javalin5`, and `javalin6`; rejected for `jaxrs-spec`).</dd><dt>**abstract**</dt><dd>Use abstract base class shaping.</dd><dt>**composition**</dt><dd>Flatten inheritance and generate discriminator owners as composition wrappers (`value: Any`).</dd></dl>|sealed (ktor, javalin5/javalin6), abstract (jaxrs-spec)|
|library|library template (sub-template)|<dl><dt>**ktor**</dt><dd>ktor framework</dd><dt>**ktor2**</dt><dd>ktor (2.x) framework</dd><dt>**jaxrs-spec**</dt><dd>JAX-RS spec only</dd><dt>**javalin5**</dt><dd>Javalin 5</dd><dt>**javalin6**</dt><dd>Javalin 6</dd></dl>|ktor|
|modelMutable|Create mutable models| |false|
|omitGradleWrapper|Whether to omit Gradle wrapper for creating a sub project.| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand Down Expand Up @@ -109,6 +110,7 @@ public class KotlinServerCodegen extends AbstractKotlinCodegen implements BeanVa
@Getter
@Setter
private Boolean delegatePatternEnabled = false;
private String inheritanceMode;

/**
* Constructs an instance of `KotlinServerCodegen`.
Expand Down Expand Up @@ -184,6 +186,17 @@ public KotlinServerCodegen() {
addSwitch(USE_JAKARTA_EE, Constants.USE_JAKARTA_EE_DESC, useJakartaEe);
addSwitch(Constants.FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE, Constants.FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE_DESC, fixJacksonJsonTypeInfoInheritance);
addSwitch(Constants.DELEGATE_PATTERN, Constants.DELEGATE_PATTERN_DESC, getDelegatePatternEnabled());
addOption(Constants.INHERITANCE_MODE, Constants.INHERITANCE_MODE_DESC,
defaultInheritanceModeForLibrary(DEFAULT_LIBRARY), buildInheritanceModeOptions());
}

private Map<String, String> buildInheritanceModeOptions() {
Map<String, String> options = new HashMap<>();
options.put(Constants.INHERITANCE_MODE_NONE, "Flat models (no inheritance-aware shaping)");
options.put(Constants.INHERITANCE_MODE_SEALED, "Sealed/interface oriented inheritance");
options.put(Constants.INHERITANCE_MODE_ABSTRACT, "Abstract base classes and concrete children");
options.put(Constants.INHERITANCE_MODE_COMPOSITION, "Composition wrappers for polymorphism");
return options;
}

@Override
Expand Down Expand Up @@ -271,6 +284,37 @@ public void processOpts() {
LOGGER.info("`library` option is empty. Default to {}", DEFAULT_LIBRARY);
}

inheritanceMode = defaultInheritanceModeForLibrary(library);
if (additionalProperties.containsKey(Constants.INHERITANCE_MODE)) {
String configuredInheritanceMode = String.valueOf(additionalProperties.get(Constants.INHERITANCE_MODE));
if (!isValidInheritanceMode(configuredInheritanceMode)) {
throw new IllegalArgumentException(String.format(Locale.ROOT,
"Invalid %s value '%s'. Supported values are: %s, %s, %s, %s",
Constants.INHERITANCE_MODE,
configuredInheritanceMode,
Constants.INHERITANCE_MODE_NONE,
Constants.INHERITANCE_MODE_SEALED,
Constants.INHERITANCE_MODE_ABSTRACT,
Constants.INHERITANCE_MODE_COMPOSITION));
}
inheritanceMode = configuredInheritanceMode;
}

if (Constants.JAXRS_SPEC.equals(library) && Constants.INHERITANCE_MODE_SEALED.equals(inheritanceMode)) {
throw new IllegalArgumentException(String.format(Locale.ROOT,
"Library '%s' does not support inheritanceMode '%s'. Use '%s', '%s', or '%s'.",
Constants.JAXRS_SPEC,
Constants.INHERITANCE_MODE_SEALED,
Constants.INHERITANCE_MODE_NONE,
Constants.INHERITANCE_MODE_ABSTRACT,
Constants.INHERITANCE_MODE_COMPOSITION));
}
additionalProperties.put(Constants.INHERITANCE_MODE, inheritanceMode);
additionalProperties.put(Constants.X_INHERITANCE_MODE_NONE, Constants.INHERITANCE_MODE_NONE.equals(inheritanceMode));
additionalProperties.put(Constants.X_INHERITANCE_MODE_SEALED, Constants.INHERITANCE_MODE_SEALED.equals(inheritanceMode));
additionalProperties.put(Constants.X_INHERITANCE_MODE_ABSTRACT, Constants.INHERITANCE_MODE_ABSTRACT.equals(inheritanceMode));
additionalProperties.put(Constants.X_INHERITANCE_MODE_COMPOSITION, Constants.INHERITANCE_MODE_COMPOSITION.equals(inheritanceMode));

if (isKtor()) {
typeMapping.put("date-time", "kotlin.String");
typeMapping.put("DateTime", "kotlin.String");
Expand Down Expand Up @@ -436,12 +480,36 @@ public static class Constants {
public static final String USE_TAGS_DESC = "use tags for creating interface and controller classnames.";
public static final String DELEGATE_PATTERN = "delegatePattern";
public static final String DELEGATE_PATTERN_DESC = "Whether to generate the server files using the delegate pattern. This option is currently supported only when using ktor library.";
public static final String INHERITANCE_MODE = "inheritanceMode";
public static final String INHERITANCE_MODE_DESC = "Strategy for model inheritance generation. Values: none, sealed, abstract, composition.";
public static final String INHERITANCE_MODE_NONE = "none";
public static final String INHERITANCE_MODE_SEALED = "sealed";
public static final String INHERITANCE_MODE_ABSTRACT = "abstract";
public static final String INHERITANCE_MODE_COMPOSITION = "composition";
public static final String X_INHERITANCE_MODE_NONE = "x-inheritance-mode-none";
public static final String X_INHERITANCE_MODE_SEALED = "x-inheritance-mode-sealed";
public static final String X_INHERITANCE_MODE_ABSTRACT = "x-inheritance-mode-abstract";
public static final String X_INHERITANCE_MODE_COMPOSITION = "x-inheritance-mode-composition";
}

@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
objs = super.postProcessAllModels(objs);

if (Constants.INHERITANCE_MODE_NONE.equals(inheritanceMode)) {
flattenInheritanceForNoneMode(objs);
return objs;
}

if (Constants.INHERITANCE_MODE_COMPOSITION.equals(inheritanceMode)) {
applyCompositionMode(objs);
return objs;
}

if (!isInheritancePostProcessingEnabled()) {
return objs;
}

// For libraries that use Jackson, set up parent-child relationships for discriminator children
// This enables proper polymorphism support with @JsonTypeInfo and @JsonSubTypes annotations
if (usesJacksonSerialization()) {
Expand Down Expand Up @@ -920,4 +988,112 @@ private static String getCommonPath(String path1, String path2) {
}
return builder.toString();
}

private static String defaultInheritanceModeForLibrary(String currentLibrary) {
return Constants.JAXRS_SPEC.equals(currentLibrary)
? Constants.INHERITANCE_MODE_ABSTRACT
: Constants.INHERITANCE_MODE_SEALED;
}

private static boolean isValidInheritanceMode(String value) {
return Constants.INHERITANCE_MODE_NONE.equals(value)
|| Constants.INHERITANCE_MODE_SEALED.equals(value)
|| Constants.INHERITANCE_MODE_ABSTRACT.equals(value)
|| Constants.INHERITANCE_MODE_COMPOSITION.equals(value);
}

private static void flattenInheritanceForNoneMode(Map<String, ModelsMap> objs) {
for (ModelsMap modelsMap : objs.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel model = modelMap.getModel();

model.setDiscriminator(null);
model.setParent(null);
model.getVendorExtensions().remove("x-parent-ctor-args");
model.getVendorExtensions().remove("x-discriminator-has-parent-properties");
model.getVendorExtensions().remove("x-discriminator-visible-true");

if (model.interfaces != null) {
model.interfaces.clear();
}

for (CodegenProperty prop : model.getAllVars()) {
prop.isInherited = false;
}
for (CodegenProperty prop : model.getVars()) {
prop.isInherited = false;
}
for (CodegenProperty prop : model.getRequiredVars()) {
prop.isInherited = false;
}
for (CodegenProperty prop : model.getOptionalVars()) {
prop.isInherited = false;
}
}
}
}

private static void applyCompositionMode(Map<String, ModelsMap> objs) {
Set<String> discriminatorOwners = new HashSet<>();
for (ModelsMap modelsMap : objs.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel model = modelMap.getModel();
if (model.getDiscriminator() != null
&& model.getDiscriminator().getMappedModels() != null
&& !model.getDiscriminator().getMappedModels().isEmpty()) {
discriminatorOwners.add(model.getClassname());
}
}
}

flattenInheritanceForNoneMode(objs);

for (ModelsMap modelsMap : objs.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel model = modelMap.getModel();
if (discriminatorOwners.contains(model.getClassname())) {
replaceWithCompositionWrapper(model);
}
}
}
}

private static void replaceWithCompositionWrapper(CodegenModel model) {
CodegenProperty wrapperValue = new CodegenProperty();
wrapperValue.baseName = "value";
wrapperValue.name = "value";
wrapperValue.dataType = "kotlin.Any";
wrapperValue.datatypeWithEnum = "kotlin.Any";
wrapperValue.required = true;
wrapperValue.isNullable = false;
wrapperValue.isReadOnly = false;

model.getVars().clear();
model.getAllVars().clear();
model.getRequiredVars().clear();
model.getOptionalVars().clear();
model.getMandatory().clear();
model.getAllMandatory().clear();
model.oneOf.clear();
model.anyOf.clear();
model.allOf.clear();

model.getVars().add(wrapperValue);
model.getAllVars().add(wrapperValue);
model.getRequiredVars().add(wrapperValue);
model.getMandatory().add("value");
model.getAllMandatory().add("value");

model.hasVars = true;
model.hasRequired = true;
model.hasOptional = false;
model.emptyVars = false;
model.getVendorExtensions().put("x-composition-wrapper", true);
}

private boolean isInheritancePostProcessingEnabled() {
// `none` and `composition` are handled in dedicated short-circuit paths.
return !Constants.INHERITANCE_MODE_NONE.equals(inheritanceMode)
&& !Constants.INHERITANCE_MODE_COMPOSITION.equals(inheritanceMode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ import java.io.Serializable
{{#discriminator}}
{{>typeInfoAnnotation}}
{{#vendorExtensions.x-discriminator-has-parent-properties}}
sealed class {{classname}}(
{{#x-inheritance-mode-abstract}}abstract class{{/x-inheritance-mode-abstract}}{{^x-inheritance-mode-abstract}}sealed class{{/x-inheritance-mode-abstract}} {{classname}}(
{{#requiredVars}}
{{>data_class_sealed_var}}{{^-last}},
{{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}},
Expand All @@ -94,7 +94,7 @@ sealed class {{classname}}(
)
{{/vendorExtensions.x-discriminator-has-parent-properties}}
{{^vendorExtensions.x-discriminator-has-parent-properties}}
sealed class {{classname}}
{{#x-inheritance-mode-abstract}}abstract class{{/x-inheritance-mode-abstract}}{{^x-inheritance-mode-abstract}}sealed class{{/x-inheritance-mode-abstract}} {{classname}}
{{/vendorExtensions.x-discriminator-has-parent-properties}}
{{/discriminator}}
{{^discriminator}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,28 @@ import java.io.Serializable
{{/isDeprecated}}
{{>additionalModelTypeAnnotations}}

{{#nonPublicApi}}internal {{/nonPublicApi}}{{#discriminator}}interface{{/discriminator}}{{^discriminator}}data class{{/discriminator}} {{classname}}{{^discriminator}} (
{{#nonPublicApi}}internal {{/nonPublicApi}}{{#discriminator}}{{#x-inheritance-mode-abstract}}abstract class{{/x-inheritance-mode-abstract}}{{^x-inheritance-mode-abstract}}interface{{/x-inheritance-mode-abstract}}{{/discriminator}}{{^discriminator}}data class{{/discriminator}} {{classname}}{{^discriminator}} (

{{#allVars}}
{{#required}}{{>data_class_req_var}}{{/required}}{{^required}}{{>data_class_opt_var}}{{/required}}{{^-last}},{{/-last}}

{{/allVars}}
){{/discriminator}}{{#parent}}{{^serializableModel}}{{^parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#parent}}{{#serializableModel}}{{^parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#parent}}{{^serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#parent}}{{#serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{#serializableModel}}{{^parcelizeModels}} : Serializable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{#serializableModel}}{{#parcelizeModels}} : Serializable, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#vendorExtensions.x-has-data-class-body}} {
){{/discriminator}}{{#discriminator}}{{#x-inheritance-mode-abstract}}(

{{#allVars}}
{{#required}}{{>data_class_sealed_var}}{{/required}}{{^required}}{{>data_class_sealed_var}}{{/required}}{{^-last}},{{/-last}}

{{/allVars}}
){{/x-inheritance-mode-abstract}}{{/discriminator}}{{#parent}}{{#vendorExtensions.x-parent-ctor-args}}{{#serializableModel}}{{#parcelizeModels}} : {{{parent}}}({{{.}}}), Serializable, Parcelable{{/parcelizeModels}}{{^parcelizeModels}} : {{{parent}}}({{{.}}}), Serializable{{/parcelizeModels}}{{/serializableModel}}{{^serializableModel}}{{#parcelizeModels}} : {{{parent}}}({{{.}}}), Parcelable{{/parcelizeModels}}{{^parcelizeModels}} : {{{parent}}}({{{.}}}){{/parcelizeModels}}{{/serializableModel}}{{/vendorExtensions.x-parent-ctor-args}}{{^vendorExtensions.x-parent-ctor-args}}{{#serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable, Parcelable{{/parcelizeModels}}{{^parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Serializable{{/parcelizeModels}}{{/serializableModel}}{{^serializableModel}}{{#parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}, Parcelable{{/parcelizeModels}}{{^parcelizeModels}} : {{{parent}}}{{#isMap}}(){{/isMap}}{{#isArray}}(){{/isArray}}{{/parcelizeModels}}{{/serializableModel}}{{/vendorExtensions.x-parent-ctor-args}}{{/parent}}{{^parent}}{{#serializableModel}}{{^parcelizeModels}} : Serializable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{^serializableModel}}{{#parcelizeModels}} : Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{^parent}}{{#serializableModel}}{{#parcelizeModels}} : Serializable, Parcelable{{/parcelizeModels}}{{/serializableModel}}{{/parent}}{{#vendorExtensions.x-has-data-class-body}} {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
{{/vendorExtensions.x-has-data-class-body}}
{{#serializableModel}}
{{#nonPublicApi}}internal {{/nonPublicApi}}companion object {
private const val serialVersionUID: Long = 123
}
{{/serializableModel}}
{{#discriminator}}{{#vars}}{{#required}}
{{#discriminator}}{{^x-inheritance-mode-abstract}}{{#vars}}{{#required}}
{{>interface_req_var}}{{/required}}{{^required}}
{{>interface_opt_var}}{{/required}}{{/vars}}{{/discriminator}}
{{>interface_opt_var}}{{/required}}{{/vars}}{{/x-inheritance-mode-abstract}}{{/discriminator}}
{{#hasEnums}}
{{#vars}}
{{#isEnum}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
{{/description}}

@JsonProperty("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}")
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
{{/description}}

@JsonProperty("{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}")
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}
{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}} {{#isInherited}}override {{/isInherited}}{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInPascalCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}
Loading
Loading