Skip to content
Merged
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 bin/configs/kotlin-spring-boot-sort-validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ additionalProperties:
useSpringBoot3: "true"
generateSortValidation: "true"
generatePageableConstraintValidation: "true"
openApiNullable: "true"
useTags: "true"
requestMappingMode: api_interface
3 changes: 2 additions & 1 deletion docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|library|library template (sub-template)|<dl><dt>**spring-boot**</dt><dd>Spring-boot Server application.</dd><dt>**spring-cloud**</dt><dd>Spring-Cloud-Feign client with Spring-Boot auto-configured settings.</dd><dt>**spring-declarative-http-interface**</dt><dd>Spring Declarative Interface client</dd></dl>|spring-boot|
|modelMutable|Create mutable models| |false|
|modelPackage|model package for generated code| |org.openapitools.model|
|openApiNullable|Enable OpenAPI Jackson Nullable library (jackson-databind-nullable) for optional + nullable properties (required: false, nullable: true). When enabled, such properties use JsonNullable&lt;T&gt; = JsonNullable.undefined() so callers can distinguish between a missing key and an explicitly provided null. Requires jackson-databind-nullable &gt;= 0.2.10 when used with useJackson3.| |false|
|packageName|Generated artifact package name.| |org.openapitools|
|parcelizeModels|toggle &quot;@Parcelize&quot; for generated models| |null|
|reactive|use coroutines for reactive behavior| |false|
Expand All @@ -64,7 +65,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useDeductionForOneOfInterfaces|Annotate discriminator-free oneOf interfaces with Jackson's @JsonTypeInfo(use = Id.DEDUCTION) and @JsonSubTypes so the concrete subtype is resolved from the JSON field set rather than a type-tag property. Has no effect when a discriminator is present (name-based resolution is used instead). Requires subtypes to have structurally distinct sets of properties.| |false|
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
|useFlowForArrayReturnType|Whether to use Flow for array/collection return types when reactive is enabled. If false, will use List instead.| |true|
|useJackson3|Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled. Incompatible with `openApiNullable`.| |false|
|useJackson3|Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled.| |false|
|useResponseEntity|Whether (when false) to return actual type (e.g. List&lt;Fruit&gt;) and handle non-happy path responses via exceptions flow or (when true) return entire ResponseEntity (e.g. ResponseEntity&lt;List&lt;Fruit&gt;&gt;). If disabled, method are annotated using a @ResponseStatus annotation, which has the status of the first response declared in the Api definition| |true|
|useSealedResponseInterfaces|Generate sealed interfaces for endpoint responses that all possible response types implement. Allows controllers to return any valid response type in a type-safe manner (e.g., sealed interface CreateUserResponse implemented by User, ConflictResponse, ErrorResponse)| |false|
|useSpringBoot3|Generate code and provide dependencies for use with Spring Boot &ge; 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ public class CodegenConstants {
public static final String NULLABLE_REFERENCE_TYPES = "nullableReferenceTypes";
public static final String NULLABLE_REFERENCE_TYPES_DESC = "Use nullable annotations in the project. Only supported on C# 8 / ASP.NET Core 3.1 or newer.";

public static final String OPENAPI_NULLABLE = "openApiNullable";
public static final String OPENAPI_NULLABLE_DESC = "Enable OpenAPI Jackson Nullable library (jackson-databind-nullable) for optional + nullable properties.";

public static final String TEMPLATING_ENGINE = "templatingEngine";
public static final String TEMPLATING_ENGINE_DESC = "The templating engine plugin to use: \"mustache\" (default) or \"handlebars\" (beta)";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
public static final String ADDITIONAL_ONE_OF_TYPE_ANNOTATIONS = "additionalOneOfTypeAnnotations";
public static final String ADDITIONAL_ENUM_TYPE_ANNOTATIONS = "additionalEnumTypeAnnotations";
public static final String DISCRIMINATOR_CASE_SENSITIVE = "discriminatorCaseSensitive";
public static final String OPENAPI_NULLABLE = "openApiNullable";
public static final String OPENAPI_NULLABLE = CodegenConstants.OPENAPI_NULLABLE;
public static final String JACKSON = "jackson";
public static final String TEST_OUTPUT = "testOutput";
public static final String IMPLICIT_HEADERS = "implicitHeaders";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ public String getDescription() {
@Setter private boolean substituteGenericPagedModel = false;
@Setter private boolean useSealedResponseInterfaces = false;
@Setter private boolean companionObject = false;
@Getter @Setter private boolean openApiNullable = false;
@Getter @Setter
protected boolean useDeductionForOneOfInterfaces = false;

Expand Down Expand Up @@ -290,7 +291,7 @@ public KotlinSpringServerCodegen() {
" (contexts) added to single project.", beanQualifiers);
addSwitch(USE_SPRING_BOOT3, "Generate code and provide dependencies for use with Spring Boot ≥ 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.", useSpringBoot3);
addSwitch(USE_SPRING_BOOT4, "Generate code and provide dependencies for use with Spring Boot 4.x. Enabling this option will also enable `useJakartaEe`.", useSpringBoot4);
addSwitch(USE_JACKSON_3, "Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled. Incompatible with `openApiNullable`.", useJackson3);
addSwitch(USE_JACKSON_3, "Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled.", useJackson3);
addSwitch(USE_FLOW_FOR_ARRAY_RETURN_TYPE, "Whether to use Flow for array/collection return types when reactive is enabled. If false, will use List instead.", useFlowForArrayReturnType);
addSwitch(INCLUDE_HTTP_REQUEST_CONTEXT, "Whether to include HttpServletRequest (blocking) or ServerWebExchange (reactive) as additional parameter in generated methods.", includeHttpRequestContext);
addSwitch(USE_RESPONSE_ENTITY,
Expand All @@ -314,6 +315,12 @@ public KotlinSpringServerCodegen() {
substituteGenericPagedModel);
addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject);
cliOptions.add(CliOption.newBoolean(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces));
addSwitch(CodegenConstants.OPENAPI_NULLABLE,
"Enable OpenAPI Jackson Nullable library (jackson-databind-nullable) for optional + nullable "
+ "properties (required: false, nullable: true). When enabled, such properties use "
+ "JsonNullable<T> = JsonNullable.undefined() so callers can distinguish between a missing key "
+ "and an explicitly provided null. Requires jackson-databind-nullable >= 0.2.10 when used with useJackson3.",
openApiNullable);
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
Expand Down Expand Up @@ -543,10 +550,14 @@ public void processOpts() {
// used later in recursive import in postProcessingModels
importMapping.put("com.fasterxml.jackson.annotation.JsonProperty", "com.fasterxml.jackson.annotation.JsonCreator");

if (isUseJackson3()) {
// Override databind imports for Jackson 3
importMapping.put("JsonDeserialize", "tools.jackson.databind.annotation.JsonDeserialize");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this was not being set anywhere until now as far as I can tell. So on jackson 2 the JsonDeserialize simply was not imported.

}
// Jackson 3.x intentionally kept jackson-annotations at 2.x (com.fasterxml.jackson.annotation).
// Only jackson-databind moved to tools.jackson.databind in Jackson 3.x.
importMapping.put("JsonSetter", "com.fasterxml.jackson.annotation.JsonSetter");
importMapping.put("Nulls", "com.fasterxml.jackson.annotation.Nulls");
// jackson-databind-nullable >= 0.2.10 supports both Jackson 2 and 3.
importMapping.put("JsonNullable", "org.openapitools.jackson.nullable.JsonNullable");
// JsonDeserialize lives in jackson-databind which moved packages in Jackson 3.x.
importMapping.put("JsonDeserialize", (isUseJackson3() ? JACKSON3_PACKAGE : JACKSON2_PACKAGE) + ".databind.annotation.JsonDeserialize");

// Spring-specific import mappings for x-spring-paginated support
importMapping.put("ParameterObject", "org.springdoc.api.annotations.ParameterObject");
Expand Down Expand Up @@ -767,10 +778,10 @@ public void processOpts() {
throw new IllegalArgumentException("useJackson3 is only available with Spring Boot >= 4");
}

if (isUseJackson3() && additionalProperties.containsKey("openApiNullable")
&& Boolean.parseBoolean(additionalProperties.get("openApiNullable").toString())) {
throw new IllegalArgumentException("openApiNullable cannot be set with useJackson3");
if (additionalProperties.containsKey(CodegenConstants.OPENAPI_NULLABLE)) {
this.setOpenApiNullable(convertPropertyToBoolean(CodegenConstants.OPENAPI_NULLABLE));
}
writePropertyBack(CodegenConstants.OPENAPI_NULLABLE, openApiNullable);

if (isUseSpringBoot3() || isUseSpringBoot4()) {
if (AnnotationLibrary.SWAGGER1.equals(getAnnotationLibrary())) {
Expand Down Expand Up @@ -1321,6 +1332,21 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
property.example = null;
}

// Scenario 3: optional + non-nullable → block explicit JSON nulls via @JsonSetter(nulls = Nulls.FAIL).
// Missing keys still succeed (default = null is used), but explicit {"field": null} fails deserialization.
if (!Boolean.TRUE.equals(property.required) && !Boolean.TRUE.equals(property.isNullable)) {
property.vendorExtensions.put("x-has-json-setter-nulls-fail", true);
model.imports.add("JsonSetter");
model.imports.add("Nulls");
}

// Scenario 4: optional + nullable with openApiNullable → use JsonNullable<T> = JsonNullable.undefined()
// so callers can distinguish between a missing key and an explicitly provided null.
if (openApiNullable && !Boolean.TRUE.equals(property.required) && Boolean.TRUE.equals(property.isNullable)) {
property.vendorExtensions.put("x-is-jackson-optional-nullable", true);
model.imports.add("JsonNullable");
}

//Add imports for Jackson
if (!Boolean.TRUE.equals(model.isEnum)) {
model.imports.add("JsonProperty");
Expand Down Expand Up @@ -1475,6 +1501,23 @@ public ModelsMap postProcessModelsEnum(ModelsMap objs) {
//Add imports for Jackson
List<Map<String, String>> imports = objs.getImports();

// Set 4-state nullable/required vendor extensions on optionalVars instances.
// optionalVars are cloned independently from vars by removeAllDuplicatedProperty(),
// so they require a separate pass here — postProcessModelProperty only covers vars.
for (ModelMap mo : objs.getModels()) {
CodegenModel cm = mo.getModel();
for (CodegenProperty var : cm.optionalVars) {
// Scenario 3: optional + non-nullable → block explicit JSON nulls via @JsonSetter(nulls = Nulls.FAIL)
if (!var.required && !var.isNullable) {
var.vendorExtensions.put("x-has-json-setter-nulls-fail", true);
}
// Scenario 4: optional + nullable with openApiNullable → use JsonNullable<T>
if (openApiNullable && !var.required && var.isNullable) {
var.vendorExtensions.put("x-is-jackson-optional-nullable", true);
}
}
}

objs.getModels().stream()
.map(ModelMap::getModel)
.filter(cm -> Boolean.TRUE.equals(cm.isEnum) && cm.allowableValues != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeInNormalString}}{{{.}}}{{/lambdaEscapeInNormalString}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeInNormalString}}{{{.}}}{{/lambdaEscapeInNormalString}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#deprecated}}
@Deprecated(message = ""){{/deprecated}}{{#vendorExtensions.x-field-extra-annotation}}
{{{.}}}{{/vendorExtensions.x-field-extra-annotation}}
@get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{^defaultValue}}null{{/defaultValue}}{{#defaultValue}}{{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}
{{{.}}}{{/vendorExtensions.x-field-extra-annotation}}{{#vendorExtensions.x-has-json-setter-nulls-fail}}
@field:JsonSetter(nulls = Nulls.FAIL){{/vendorExtensions.x-has-json-setter-nulls-fail}}
@get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#vendorExtensions.x-is-jackson-optional-nullable}}JsonNullable<{{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}>{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}?{{/vendorExtensions.x-is-jackson-optional-nullable}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}JsonNullable.undefined(){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^defaultValue}}null{{/defaultValue}}{{#defaultValue}}{{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/vendorExtensions.x-is-jackson-optional-nullable}}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ dependencies {
{{#useBeanValidation}}
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
{{#openApiNullable}}
implementation("org.openapitools:jackson-databind-nullable:0.2.10"){{/openApiNullable}}
implementation("jakarta.annotation:jakarta.annotation-api:2.1.0")

testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ dependencies {
{{#useBeanValidation}}
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
{{#openApiNullable}}
implementation("org.openapitools:jackson-databind-nullable:0.2.10"){{/openApiNullable}}
implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")

testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ dependencies {
{{#useBeanValidation}}
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("javax.validation:validation-api"){{/useBeanValidation}}
{{#openApiNullable}}
implementation("org.openapitools:jackson-databind-nullable:0.2.8"){{/openApiNullable}}
implementation("javax.annotation:javax.annotation-api:1.3.2")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<swagger-annotations.version>2.2.15</swagger-annotations.version>{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
<findbugs-jsr305.version>3.0.2</findbugs-jsr305.version>
<jakarta-annotation.version>2.1.0</jakarta-annotation.version>
<kotlin-test-junit5.version>1.7.10</kotlin-test-junit5.version>
<kotlin-test-junit5.version>1.9.25</kotlin-test-junit5.version>

<kotlin.version>1.7.10</kotlin.version>
<kotlin.version>1.9.25</kotlin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
Expand Down Expand Up @@ -197,12 +197,23 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>{{/useBeanValidation}}
{{#openApiNullable}}
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.10</version>
</dependency>{{/openApiNullable}}
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>${jakarta-annotation.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>{{/useBeanValidation}}
{{#openApiNullable}}
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.10</version>
</dependency>{{/openApiNullable}}
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
Expand All @@ -215,5 +221,10 @@
<version>${kotlin-test-junit5.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>{{/useBeanValidation}}
{{#openApiNullable}}
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.8</version>
</dependency>{{/openApiNullable}}
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
Expand All @@ -196,5 +202,10 @@
<version>${kotlin-test-junit5.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Loading
Loading