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 index 5d49f11e4..7c1b582b8 100644 --- 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 @@ -128,8 +128,15 @@ class KotlinNullablePropertyCustomizer( * Marks a non-$ref property as nullable. * - OAS 3.0: `nullable: true` * - OAS 3.1: adds `"null"` to the `types` set + * + * A schema representing `Any?` carries no `type`/`types` constraint, i.e. it is an + * "any" schema which already permits any value including `null`. Such a schema is left + * untouched (kept as an empty schema) rather than being narrowed to `type: "null"` + * (3.1) or decorated with a redundant `nullable: true` (3.0). */ private fun markNullable(property: Schema<*>, specVersion: SpecVersion) { + if (isAnySchema(property)) return + if (specVersion == SpecVersion.V31) { val currentTypes = property.types ?: property.type?.let { setOf(it) } ?: emptySet() if ("null" !in currentTypes) { @@ -140,6 +147,16 @@ class KotlinNullablePropertyCustomizer( } } + /** + * Returns true when the schema imposes no type constraint (i.e. represents `Any`), + * in which case it is treated as an empty schema that already allows any value, + * including `null`. + */ + private fun isAnySchema(property: Schema<*>): Boolean = + property.`$ref` == null && + property.type == null && + property.types.isNullOrEmpty() + /** * Wraps a $ref property in a nullable composite schema. A fresh wrapper schema is returned * (the original property object is left untouched) with any sibling attributes such as 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 index b190333fa..88d11b2f0 100644 --- 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 @@ -8,6 +8,7 @@ data class NullableFieldsResponse( val requiredField: String, val nullableString: String? = null, val nullableInt: Int? = null, + val nullableAny: Any? = null, @field:Schema(description = "The nested object") val nullableNested: NestedObject? = null, ) diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app19/NullableController.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app19/NullableController.kt index 5befb6ecc..cbb40c68f 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app19/NullableController.kt +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v30/app19/NullableController.kt @@ -7,6 +7,7 @@ data class NullableFieldsResponse( val requiredField: String, val nullableString: String? = null, val nullableInt: Int? = null, + val nullableAny: Any? = null, val nullableNested: NestedObject? = null, ) 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 index 9061d33b0..8caecbdc3 100644 --- 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 @@ -8,6 +8,7 @@ data class NullableFieldsResponse( val requiredField: String, val nullableString: String? = null, val nullableInt: Int? = null, + val nullableAny: Any? = null, @field:Schema(description = "The nested object") val nullableNested: NestedObject? = null, ) diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app24/NullableController.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app24/NullableController.kt index f3692c404..9fae28b65 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app24/NullableController.kt +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/v31/app24/NullableController.kt @@ -7,6 +7,7 @@ data class NullableFieldsResponse( val requiredField: String, val nullableString: String? = null, val nullableInt: Int? = null, + val nullableAny: Any? = null, val nullableNested: NestedObject? = null, ) 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 index c01658b44..cbcb8e772 100644 --- 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 @@ -68,6 +68,10 @@ "format": "int32", "nullable": true }, + "nullableAny" : { + "type" : "object", + "nullable" : true + }, "nullableNested": { "nullable": true, "allOf": [ diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.0.1/app19.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.0.1/app19.json index 1e2388013..c0c18ec03 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.0.1/app19.json +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.0.1/app19.json @@ -64,6 +64,9 @@ "type": "integer", "format": "int32" }, + "nullableAny" : { + "type" : "object" + }, "nullableNested": { "$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 index bc3974feb..f25a1d44f 100644 --- 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 @@ -70,6 +70,7 @@ ], "format": "int32" }, + "nullableAny": { }, "nullableNested": { "description": "The nested object", "oneOf": [ diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.1.0/app24.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.1.0/app24.json index 12b512a12..d71921666 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.1.0/app24.json +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/3.1.0/app24.json @@ -61,6 +61,7 @@ "type": "integer", "format": "int32" }, + "nullableAny": { }, "nullableNested": { "$ref": "#/components/schemas/NestedObject" }