diff --git a/bin/configs/kotlin-spring-boot-sort-validation.yaml b/bin/configs/kotlin-spring-boot-sort-validation.yaml
index 6f543e0cf4d5..485233c40fad 100644
--- a/bin/configs/kotlin-spring-boot-sort-validation.yaml
+++ b/bin/configs/kotlin-spring-boot-sort-validation.yaml
@@ -14,5 +14,6 @@ additionalProperties:
useSpringBoot3: "true"
generateSortValidation: "true"
generatePageableConstraintValidation: "true"
+ openApiNullable: "true"
useTags: "true"
requestMappingMode: api_interface
diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md
index 584c4e1c9cb3..9838ac83a85b 100644
--- a/docs/generators/kotlin-spring.md
+++ b/docs/generators/kotlin-spring.md
@@ -44,6 +44,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|library|library template (sub-template)|
- **spring-boot**
- Spring-boot Server application.
- **spring-cloud**
- Spring-Cloud-Feign client with Spring-Boot auto-configured settings.
- **spring-declarative-http-interface**
- Spring Declarative Interface client
|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<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.| |false|
|packageName|Generated artifact package name.| |org.openapitools|
|parcelizeModels|toggle "@Parcelize" for generated models| |null|
|reactive|use coroutines for reactive behavior| |false|
@@ -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<Fruit>) and handle non-happy path responses via exceptions flow or (when true) return entire ResponseEntity (e.g. ResponseEntity<List<Fruit>>). 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 ≥ 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java
index b2a4de1201e9..713bc8f7bf9f 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java
@@ -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)";
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java
index 6cf3ca55fee2..b4d025415f18 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java
@@ -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";
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java
index e3315a25cea7..7bc6bb393a8d 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java
@@ -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;
@@ -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,
@@ -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 = 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.");
@@ -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");
- }
+ // 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");
@@ -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())) {
@@ -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 = 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");
@@ -1475,6 +1501,23 @@ public ModelsMap postProcessModelsEnum(ModelsMap objs) {
//Add imports for Jackson
List