Parameter is no longer optional after upgrade to 2.8.8
Describe the bug
After upgrading from 2.8.6 to 2.8.8, request parameters that were previously correctly marked as required: false are now marked as required: true
To Reproduce Steps to reproduce the behavior:
- What version of spring-boot you are using?
- 3.4.5
- What modules and versions of springdoc-openapi are you using?
- springdoc-openapi-starter-webmvc-ui 2.8.8
- Provide with a sample code (HelloController) or Test that reproduces the problem
@ParameterObject
data class FooParameters(
@RequestParam(name = "bar", required = false, defaultValue = DEFAULT_BAR.toString())
@field:Parameter(schema = Schema(minimum = "1", type = "integer", defaultValue = DEFAULT_BAR.toString()))
@field:Min(1)
val bar: Int = DEFAULT_BAR,
@RequestParam(name = "baz", required = false, defaultValue = DEFAULT_BAZ.toString())
@field:Parameter(schema = Schema(minimum = "1", maximum = "200", type = "integer", defaultValue = DEFAULT_BAZ.toString()))
@field:Min(1)
@field:Max(200)
val baz: Int = DEFAULT_BAZ
) {
companion object {
const val DEFAULT_BAR = 1
const val DEFAULT_BAZ = 20
}
}
@RestController("/")
class FooController {
@GetMapping("/foo")
fun getFoo(@Valid fooParameters: FooParameters): String {
return "Ok"
}
}
Expected behavior (in 2.8.6)
"parameters": [
{
"name": "bar",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 1,
"minimum": 1
}
},
{
"name": "baz",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 20,
"maximum": 200,
"minimum": 1
}
}
],
Actual behavior (in 2.8.8)
"parameters": [
{
"name": "bar",
"in": "query",
"required": true,
"schema": {
"type": "integer",
"default": 1,
"minimum": 1
}
},
{
"name": "baz",
"in": "query",
"required": true,
"schema": {
"type": "integer",
"default": 20,
"maximum": 200,
"minimum": 1
}
}
]
We noticed this too. The problem is that some Kotlin-specific customizers were deprecated/removed so parameters with a default value are seen as mandatory in the JVM constructor. To solve the problem, we added the following customizer configuration.
@Configuration
class KotlinDefaultParameterCustomizer
{
@Bean
fun kotlinDefaultsInParamObjects(): DelegatingMethodParameterCustomizer =
DelegatingMethodParameterCustomizer { _, mp->
val kProp = mp.containingClass.kotlin.primaryConstructor
?.parameters
?.firstOrNull { it.name == mp.parameterName }
if (kProp?.isOptional == true)
(mp as DelegatingMethodParameter).isNotRequired= true
}
@Bean
fun kotlinDefaultsCustomizer(): ParameterCustomizer =
ParameterCustomizer { model, mp ->
if (mp.toKParameterOrNull()?.isOptional == true)
model.required = false
model
}
private fun MethodParameter.toKParameterOrNull(): KParameter? {
if (parameterIndex < 0) return null
val kFunc = method?.kotlinFunction ?: return null
return kFunc.parameters.getOrNull(parameterIndex + 1)
}
}
cause https://github.com/springdoc/springdoc-openapi/blob/bce44dbe502cbaeeff6188fe210042859aa0ed54/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/SchemaUtils.java#L180
I agree that using DelegatingMethodParameterCustomizer to customize field requirement is appropriate.
Regarding language-specific implementations:
For Kotlin:
If you decompile Kotlin code to Java using IntelliJ's Kotlin Bytecode tool, you'll see fields annotated with either org.jetbrains.annotations.NotNull or org.jetbrains.annotations.Nullable to indicate requirement status.
For Java: There's no native way to declare nullability, so by default fields are treated as not required. To modify this behavior:
- Use
@Nullable(from Spring or JSpecify) to mark non-required fields - Use
@NonNull(from JSpecify) to explicitly require fields
Should default values affect requirement status? No. The annotation alone should determine nullability:
@Nullablefields can receivenullvalues- Non-annotated or
@NotNullfields prohibitnullvalues
This approach decouples default values from nullability semantics - even if a field has a default value, @Nullable still permits explicit null assignments, if we do need a null.
if we do need fix you issue, we shoud add a function to get filedDefaultValue:
Quick fix worked for me, at least for request body parameters, to add @Schema(required = false, defaultValue = "value") to a field.
Should default values affect requirement status? No. The annotation alone should determine nullability:
How kotlin and jackson actually work, they allow omitting field (which has non-nullable type with default value) in request. And this is logical behavior. Library docs state it supports kotlin types, so I expect it to handle this state as well. I may not want to declare field nullable (hence allowing explicit null).
Quick fix worked for me, at least for request body parameters, to add
@Schema(required = false, defaultValue = "value")to a field.快速修复对我有用,至少对于添加到@Schema(required = false, defaultValue = "value")字段的请求正文参数有用。Should default values affect requirement status?默认值是否应该影响需求状态? No. The annotation alone should determine nullability:不。注释本身应该确定可为 null 性:
How kotlin and jackson actually work, they allow omitting field (which has non-nullable type with default value) in request. And this is logical behavior. Library docs state it supports kotlin types, so I expect it to handle this state as well. I may not want to declare field nullable (hence allowing explicit null).kotlin 和 jackson 的实际工作方式是,它们允许在 request 中省略 field(具有默认值的不可为 null 类型)。这是合乎逻辑的行为。库文档指出它支持 kotlin 类型,所以我希望它也能处理这种状态。我可能不想声明字段可为 null(因此允许显式 null)。
if do so, how can we pass a null value to framework to set database column = null for a put request.
- filed?: String
notRequired. you can pass value:
nullorstring
- field: String
required. you can pass value:
string, null throw a error.
for the spring framework: https://docs.spring.io/spring-framework/reference/languages/kotlin/annotations.html
The Spring Framework also takes advantage of Kotlin null-safety to determine if an HTTP parameter is required without having to explicitly define the required attribute. That means @ RequestParam name: String? is treated as not required and, conversely, @ RequestParam name: String is treated as being required. This feature is also supported on the Spring Messaging @ Header annotation.
Today, I upgraded from 2.7.0 (with Spring Boot 3.4.6) to 2.8.9 (with Spring Boot 3.5.0)
The openapi docs for my REST controllers are generated correctly, although the required properties are at the bottom now, instead of at the top:
2.7.0:
"AddressRequest" : {
"type" : "object",
"required" : [ "city", "houseNumber", "postalCode", "streetName" ],
"properties" : {
"city" : {
"type" : "string"
},
"houseNumber" : {
"type" : "integer",
"format" : "int32"
},
"postalCode" : {
"type" : "string"
},
"streetName" : {
"type" : "string"
}
},
"title" : "AddressRequest"
}
2.8.9:
"AddressRequest" : {
"type" : "object",
"properties" : {
"city" : {
"type" : "string"
},
"houseNumber" : {
"type" : "integer",
"format" : "int32"
},
"postalCode" : {
"type" : "string"
},
"streetName" : {
"type" : "string"
}
},
"required" : [ "city", "houseNumber", "postalCode", "streetName" ],
"title" : "AddressRequest"
}
This doesn't matter, of course, but might help you get an idea of where to look for my real problem:
I also use springwolf-sns, version 1.14.0, to generate the openapi for my events. Ever since I upgraded to springdoc-openapi 2.8.9, the "required" field is completely gone from the openapi components. This only happens to the events api, not the rest api.
The version number of springwolf-sns has not changed.
Perhaps this is related to the problem in this issue?
In a somewhat related manner, I upgraded spring boot from 3.2.5 to 3.5.2, and alongside upgraded springdoc-openapi-starter-webmvc-ui from 2.2.0 to 2.8.9, and all of my schemas keys are now marked as optionals.
So a bit of the reverse problem from OP, but I suspect it is most likely related.
I mark my fields as Nullable or Nonnull using the jakarta.annotation annotations, and it used to work as expected, but no longer does post update.
After further tests, it seems downgrading springdoc-openapi-starter-webmvc-ui to 2.7.0 allows the fields to become mark as required or not as expected. So the change of behaviour probably occured in 2.8.X Maybe alongside the open api 3.1 support ?
Fix added for the reported sample.