diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocKotlinConfiguration.kt b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocKotlinConfiguration.kt index 86dce004a..090da8f66 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocKotlinConfiguration.kt +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocKotlinConfiguration.kt @@ -28,7 +28,6 @@ package org.springdoc.core.configuration import org.springdoc.core.converters.KotlinInlineClassUnwrappingConverter import org.springdoc.core.customizers.KotlinDeprecatedPropertyCustomizer -import org.springdoc.core.customizers.KotlinNullablePropertyCustomizer import org.springdoc.core.providers.ObjectMapperProvider import org.springdoc.core.utils.Constants import org.springdoc.core.utils.SpringDocKotlinUtils @@ -78,13 +77,6 @@ class SpringDocKotlinConfiguration() { return KotlinDeprecatedPropertyCustomizer(objectMapperProvider) } - @Bean - @Lazy(false) - @ConditionalOnMissingBean - fun kotlinNullablePropertyCustomizer(objectMapperProvider: ObjectMapperProvider): KotlinNullablePropertyCustomizer { - return KotlinNullablePropertyCustomizer(objectMapperProvider) - } - @Bean @Lazy(false) @ConditionalOnMissingBean diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/KotlinNullablePropertyCustomizer.kt b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/KotlinNullablePropertyCustomizer.kt deleted file mode 100644 index 177750a2c..000000000 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/customizers/KotlinNullablePropertyCustomizer.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * - * * - * * * - * * * * - * * * * * - * * * * * * Copyright 2019-2026 the original author or authors. - * * * * * * - * * * * * * Licensed under the Apache License, Version 2.0 (the "License"); - * * * * * * you may not use this file except in compliance with the License. - * * * * * * You may obtain a copy of the License at - * * * * * * - * * * * * * https://www.apache.org/licenses/LICENSE-2.0 - * * * * * * - * * * * * * Unless required by applicable law or agreed to in writing, software - * * * * * * distributed under the License is distributed on an "AS IS" BASIS, - * * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * * * * * See the License for the specific language governing permissions and - * * * * * * limitations under the License. - * * * * * - * * * * - * * * - * * - * - */ - -package org.springdoc.core.customizers - -import com.fasterxml.jackson.databind.JavaType -import io.swagger.v3.core.converter.AnnotatedType -import io.swagger.v3.core.converter.ModelConverter -import io.swagger.v3.core.converter.ModelConverterContext -import io.swagger.v3.oas.models.Components -import io.swagger.v3.oas.models.SpecVersion -import io.swagger.v3.oas.models.media.Schema -import org.springdoc.core.providers.ObjectMapperProvider -import kotlin.reflect.full.memberProperties - -/** - * Marks schema properties as nullable for Kotlin data class fields whose - * return type is marked nullable (`Type?`). - * - * Handles both OAS 3.0 and OAS 3.1 nullable semantics: - * - **OAS 3.0**: Sets `nullable: true` on the property. For `$ref` properties, - * wraps in `allOf` since `$ref` and `nullable` are mutually exclusive. - * - **OAS 3.1**: Adds `"null"` to the `type` array. For `$ref` properties, - * wraps in `oneOf` with a `type: "null"` alternative. - * - * See: https://github.com/springdoc/springdoc-openapi/issues/906 - * - * @author Jeffrey Blayney - */ -class KotlinNullablePropertyCustomizer( - private val objectMapperProvider: ObjectMapperProvider -) : ModelConverter { - - override fun resolve( - type: AnnotatedType, - context: ModelConverterContext, - chain: Iterator - ): Schema<*>? { - if (!chain.hasNext()) return null - val resolvedSchema = chain.next().resolve(type, context, chain) - - val javaType: JavaType = - objectMapperProvider.jsonMapper().constructType(type.type) - if (javaType.rawClass.packageName.startsWith("java.")) { - return resolvedSchema - } - - val kotlinClass = try { - javaType.rawClass.kotlin - } catch (_: Throwable) { - return resolvedSchema - } - - val targetSchema = if (resolvedSchema != null && resolvedSchema.`$ref` != null) { - context.getDefinedModels()[resolvedSchema.`$ref`.substring(Components.COMPONENTS_SCHEMAS_REF.length)] - } else { - resolvedSchema - } - - if (targetSchema?.properties == null) return resolvedSchema - - val specVersion = targetSchema.specVersion ?: SpecVersion.V30 - - val replacements = mutableMapOf>() - for (prop in kotlinClass.memberProperties) { - if (!prop.returnType.isMarkedNullable) continue - val fieldName = prop.name - val property = targetSchema.properties[fieldName] ?: continue - - if (property.`$ref` != null) { - replacements[fieldName] = wrapRefNullable(property.`$ref`, specVersion) - } else { - markNullable(property, specVersion) - } - } - - replacements.forEach { (name, wrapper) -> - targetSchema.properties[name] = wrapper - } - - return resolvedSchema - } - - /** - * Marks a non-$ref property as nullable. - * - OAS 3.0: `nullable: true` - * - OAS 3.1: adds `"null"` to the `types` set - */ - private fun markNullable(property: Schema<*>, specVersion: SpecVersion) { - if (specVersion == SpecVersion.V31) { - val currentTypes = property.types ?: property.type?.let { setOf(it) } ?: emptySet() - if ("null" !in currentTypes) { - property.types = currentTypes + "null" - } - } else { - property.nullable = true - } - } - - /** - * Wraps a $ref in a nullable composite schema. - * - OAS 3.0: `{ nullable: true, allOf: [{ $ref: "..." }] }` - * - OAS 3.1: `{ oneOf: [{ $ref: "..." }, { type: "null" }] }` - */ - private fun wrapRefNullable(ref: String, specVersion: SpecVersion): Schema<*> { - val refSchema = Schema().apply { `$ref` = ref } - return if (specVersion == SpecVersion.V31) { - Schema().apply { - oneOf = listOf(refSchema, Schema().apply { addType("null") }) - } - } else { - Schema().apply { - nullable = true - allOf = listOf(refSchema) - } - } - } -} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app18/NullableController.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app18/NullableController.kt deleted file mode 100644 index ddf655181..000000000 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app18/NullableController.kt +++ /dev/null @@ -1,23 +0,0 @@ -package test.org.springdoc.api.v30.app18 - -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RestController - -data class NullableFieldsResponse( - val requiredField: String, - val nullableString: String? = null, - val nullableInt: Int? = null, - val nullableNested: NestedObject? = null, -) - -data class NestedObject( - val name: String, - val description: String? = null, -) - -@RestController -class NullableController { - @GetMapping("/nullable") - fun getNullableFields(): NullableFieldsResponse = - NullableFieldsResponse(requiredField = "hello") -} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app18/SpringDocApp18Test.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app18/SpringDocApp18Test.kt deleted file mode 100644 index 04785bedc..000000000 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app18/SpringDocApp18Test.kt +++ /dev/null @@ -1,12 +0,0 @@ -package test.org.springdoc.api.v30.app18 - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.context.annotation.ComponentScan -import test.org.springdoc.api.v30.AbstractKotlinSpringDocMVCTest - -class SpringDocApp18Test : AbstractKotlinSpringDocMVCTest() { - - @SpringBootApplication - @ComponentScan(basePackages = ["org.springdoc", "test.org.springdoc.api.v30.app18"]) - class DemoApplication -} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app23/NullableController.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app23/NullableController.kt deleted file mode 100644 index 2ee82dec4..000000000 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app23/NullableController.kt +++ /dev/null @@ -1,23 +0,0 @@ -package test.org.springdoc.api.v31.app23 - -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RestController - -data class NullableFieldsResponse( - val requiredField: String, - val nullableString: String? = null, - val nullableInt: Int? = null, - val nullableNested: NestedObject? = null, -) - -data class NestedObject( - val name: String, - val description: String? = null, -) - -@RestController -class NullableController { - @GetMapping("/nullable") - fun getNullableFields(): NullableFieldsResponse = - NullableFieldsResponse(requiredField = "hello") -} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app23/SpringDocApp23Test.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app23/SpringDocApp23Test.kt deleted file mode 100644 index ba386a6dc..000000000 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app23/SpringDocApp23Test.kt +++ /dev/null @@ -1,12 +0,0 @@ -package test.org.springdoc.api.v31.app23 - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.context.annotation.ComponentScan -import test.org.springdoc.api.v31.AbstractKotlinSpringDocMVCTest - -class SpringDocApp23Test : AbstractKotlinSpringDocMVCTest() { - - @SpringBootApplication - @ComponentScan(basePackages = ["org.springdoc", "test.org.springdoc.api.v31.app23"]) - class DemoApplication -} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.0.1/app18.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.0.1/app18.json deleted file mode 100644 index 2cc4d8b38..000000000 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.0.1/app18.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "OpenAPI definition", - "version": "v0" - }, - "servers": [ - { - "url": "http://localhost", - "description": "Generated server url" - } - ], - "paths": { - "/nullable": { - "get": { - "tags": [ - "nullable-controller" - ], - "operationId": "getNullableFields", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/NullableFieldsResponse" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "NestedObject": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string", - "nullable": true - } - } - }, - "NullableFieldsResponse": { - "required": [ - "requiredField" - ], - "type": "object", - "properties": { - "requiredField": { - "type": "string" - }, - "nullableString": { - "type": "string", - "nullable": true - }, - "nullableInt": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "nullableNested": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/NestedObject" - } - ] - } - } - } - } - } -} diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.1.0/app23.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.1.0/app23.json deleted file mode 100644 index 7249ceae6..000000000 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.1.0/app23.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "OpenAPI definition", - "version": "v0" - }, - "servers": [ - { - "url": "http://localhost", - "description": "Generated server url" - } - ], - "paths": { - "/nullable": { - "get": { - "tags": [ - "nullable-controller" - ], - "operationId": "getNullableFields", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/NullableFieldsResponse" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "NestedObject": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "name" - ] - }, - "NullableFieldsResponse": { - "type": "object", - "properties": { - "requiredField": { - "type": "string" - }, - "nullableString": { - "type": [ - "string", - "null" - ] - }, - "nullableInt": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "nullableNested": { - "oneOf": [ - { - "$ref": "#/components/schemas/NestedObject" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "requiredField" - ] - } - } - } -}