Description
Describe the bug
When springdoc-openapi-kotlin
is in the classpath, the type-derived nullability (e.g. String
vs String?
) takes precedence over the @Schema
annotation for determining whether a field is required or not.
For example, @field:Schema(required=true) member: String?
is not a required member in the generated schema.
To Reproduce
Steps to reproduce the behavior:
- What version of spring-boot you are using?
2.5.5 - What modules and versions of springdoc-openapi are you using?
org,springdoc:springdoc-openapi-ui:1.5.10
andorg,springdoc:springdoc-openapi-kotlin:1.5.10
- What is the actual and the expected result using OpenAPI Description (yml or json)?
I expect that nullable Kotlin members are non-required by default; non-nullable Kotlin member required.
However, the value for required
in a @Schema
annotation should override that.
This is not the case: When springdoc-openapi-kotlin
is in the classpath, the annotation's value is completely ignored.
- Provide with a sample code (HelloController) or Test that reproduces the problem
import io.swagger.v3.oas.annotations.media.Schema
data class ExampleDto(
@field:Schema(description = "Should be required")
val nonNullable: String,
@field:Schema(description = "Should not be required")
val nullable: String?,
@field:Schema(required = true, description = "Should be required")
val nonNullableAnnotatedRequired: String,
@field:Schema(required = false, description = "Should not be required")
val nonNullableAnnotatedNotRequired: String,
@field:Schema(required = true, description = "Should be required")
val nullableAnnotatedRequired: String?,
@field:Schema(required = false, description = "Should not be required")
val nullableAnnotatedNotRequired: String?
)
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class ExampleController {
@GetMapping("/example")
fun getExampleDto(): ExampleDto = ExampleDto("", "", "", "", "", "")
}
Expected behavior
-
A clear and concise description of what you expected to happen.
I expect thatnonNullable
,nonNullableAnnotatedRequired
, andnullableAnnotatedRequired
are considered as "required" in the generated schema, while the remaining members are not required. -
What is the expected result using OpenAPI Description (yml or json)?
Expected result:
{
"openapi":"3.0.1",
"info":{
"title":"OpenAPI definition",
"version":"v0"
},
"servers":[
{
"url":"http://localhost:8080",
"description":"Generated server url"
}
],
"paths":{
"/example":{
"get":{
"tags":[
"example-controller"
],
"operationId":"getExampleDto",
"responses":{
"200":{
"description":"OK",
"content":{
"*/*":{
"schema":{
"$ref":"#/components/schemas/ExampleDto"
}
}
}
}
}
}
}
},
"components":{
"schemas":{
"ExampleDto":{
"required":[
"nonNullable",
"nullableAnnotatedRequired",
"nonNullableAnnotatedRequired"
],
"type":"object",
"properties":{
"nonNullable":{
"type":"string",
"description":"Should be required"
},
"nullable":{
"type":"string",
"description":"Should not be required"
},
"nonNullableAnnotatedRequired":{
"type":"string",
"description":"Should be required"
},
"nonNullableAnnotatedNotRequired":{
"type":"string",
"description":"Should not be required"
},
"nullableAnnotatedRequired":{
"type":"string",
"description":"Should be required"
},
"nullableAnnotatedNotRequired":{
"type":"string",
"description":"Should not be required"
}
}
}
}
}
}
Result with springdoc-openapi-kotlin
in the classpath:
{
"openapi":"3.0.1",
"info":{
"title":"OpenAPI definition",
"version":"v0"
},
"servers":[
{
"url":"http://localhost:8080",
"description":"Generated server url"
}
],
"paths":{
"/example":{
"get":{
"tags":[
"example-controller"
],
"operationId":"getExampleDto",
"responses":{
"200":{
"description":"OK",
"content":{
"*/*":{
"schema":{
"$ref":"#/components/schemas/ExampleDto"
}
}
}
}
}
}
}
},
"components":{
"schemas":{
"ExampleDto":{
"required":[
"nonNullable",
"nonNullableAnnotatedNotRequired",
"nonNullableAnnotatedRequired"
],
"type":"object",
"properties":{
"nonNullable":{
"type":"string",
"description":"Should be required"
},
"nullable":{
"type":"string",
"description":"Should not be required"
},
"nonNullableAnnotatedRequired":{
"type":"string",
"description":"Should be required"
},
"nonNullableAnnotatedNotRequired":{
"type":"string",
"description":"Should not be required"
},
"nullableAnnotatedRequired":{
"type":"string",
"description":"Should be required"
},
"nullableAnnotatedNotRequired":{
"type":"string",
"description":"Should not be required"
}
}
}
}
}
}
Result without springdoc-openapi-kotlin
in the classpath:
{
"openapi":"3.0.1",
"info":{
"title":"OpenAPI definition",
"version":"v0"
},
"servers":[
{
"url":"http://localhost:8080",
"description":"Generated server url"
}
],
"paths":{
"/example":{
"get":{
"tags":[
"example-controller"
],
"operationId":"getExampleDto",
"responses":{
"200":{
"description":"OK",
"content":{
"*/*":{
"schema":{
"$ref":"#/components/schemas/ExampleDto"
}
}
}
}
}
}
}
},
"components":{
"schemas":{
"ExampleDto":{
"required":[
"nonNullableAnnotatedRequired",
"nullableAnnotatedRequired"
],
"type":"object",
"properties":{
"nonNullable":{
"type":"string",
"description":"Should be required"
},
"nullable":{
"type":"string",
"description":"Should not be required"
},
"nonNullableAnnotatedRequired":{
"type":"string",
"description":"Should be required"
},
"nonNullableAnnotatedNotRequired":{
"type":"string",
"description":"Should not be required"
},
"nullableAnnotatedRequired":{
"type":"string",
"description":"Should be required"
},
"nullableAnnotatedNotRequired":{
"type":"string",
"description":"Should not be required"
}
}
}
}
}
}
Additional context
Sample repository with springdoc-openapi-kotlin
: https://github.com/Clubfan22/springdoc-openapi-kotlin-required-ignored/tree/with-springdoc-openapi-kotlin
Sample repository without springdoc-openapi-kotlin
: https://github.com/Clubfan22/springdoc-openapi-kotlin-required-ignored/tree/without-springdoc-openapi-kotlin
SpringDoc-OpenApi's ModelResolver
uses Jackson for determining some properties of classes,
i.e. members, methods and annotations.
Therefore, SpringDoc-OpenApi extends Swagger-core's AbstractModelConverter
in the ModelResolver
class.
and registers SwaggerAnnotationIntrospector
with its object mapper.
SwaggerAnnotationIntrospector
is used for reading the @Schema
annotation's values.
However, if SpringDoc-OpenApi-Kotlin is on the classpath (!), its static code block is executed.
Within that code block, it registers the KotlinModule
of Jackson with the ObjectMapper
instance used by
ModelResolver
. Because KotlinModule
's setup method uses insertAnnotationIntrospector
,
the KotlinAnnotationIntrospector
becomes the new default.
Consequently, when the ModelResolver
tries to determine whether a class member is required,
now the KotlinAnnotationIntrospector
is always used. It returns true for non-nullable types, false otherwise.
Thus, the SwaggerAnnotationIntrospector
is never called and the @Schema
annotation is never checked.
By not loading SpringDoc-OpenApi-Kotlin, this problem is avoided and @Schema
annotations are honored.
However, nullable kotlin types are not automatically marked as not required.