springdoc-openapi icon indicating copy to clipboard operation
springdoc-openapi copied to clipboard

Parameter is no longer optional after upgrade to 2.8.8

Open egrimstad opened this issue 8 months ago • 4 comments

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
    }
  }
]

egrimstad avatar May 05 '25 06:05 egrimstad

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)
    }
}

jvanderhoek avatar May 06 '25 10:05 jvanderhoek

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:

  • @Nullable fields can receive null values
  • Non-annotated or @NotNull fields prohibit null values

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:

Image

mymx2 avatar May 07 '25 15:05 mymx2

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).

s-volkov-1 avatar May 27 '25 13:05 s-volkov-1

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: null or string

  • 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.

mymx2 avatar May 28 '25 09:05 mymx2

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?

gertjanschouten avatar Jun 19 '25 15:06 gertjanschouten

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.

jonathanSimonney avatar Jul 09 '25 15:07 jonathanSimonney

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 ?

jonathanSimonney avatar Jul 09 '25 16:07 jonathanSimonney

Fix added for the reported sample.

bnasslahsen avatar Aug 18 '25 22:08 bnasslahsen