Skip to content

@Schema(required=true/false) ignored when springdoc-openapi-kotlin is in classpath #1285

Closed
@Clubfan22

Description

@Clubfan22

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 and org,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 that nonNullable, nonNullableAnnotatedRequired, and nullableAnnotatedRequired 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    wontfixThis will not be worked on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions