[kotlin-spring] Bean Validation and custom annotations on collection element types

Merged
PI
5d ago
+4209-162

Today kotlin-spring ignores constraints on array items:

val tags: kotlin.collections.List<kotlin.String>? = null   // items: maxLength: 10 is ignored
val pets: kotlin.collections.List<Pet>? = null              // nested Pet is never validated

With this PR (and useBeanValidation=true):

val tags: kotlin.collections.List<@Size(max = 10) kotlin.String>? = null
val pets: kotlin.collections.List<@Valid Pet>? = null

This follows the existing Java logic in AbstractJavaCodegen (getTypeDeclarationWithBeanValidation / getBeanValidation / getExtraListAnnotation).

What changes

  • AbstractKotlinCodegen.getTypeDeclaration(): for arrays and sets, the element type is rendered as <extra annotations> <bean validation> Type[?]. The protected hooks behind it are no-ops by default, so other Kotlin generators (kotlin client, kotlin-server, kotlin-wiremock, …) produce identical output. Only KotlinSpringServerCodegen overrides them. A useBeanValidationOnMapValueType() hook (default false) is ready for a follow-up on map values.
  • Bean validation on elements (useBeanValidation=true):
    • $ref to object or composed (oneOf/anyOf/allOf, including normalizer-wrapped allOf: [$ref]) models → @Valid
    • string / number / integer / long constraints → @Size, @Pattern, @Email, @Min, @Max, @DecimalMin, @DecimalMax
    • enum and free-form object items → no annotation
  • x-field-extra-annotation on items (string or list) is added to the element type. This also works for inline enum items.
  • New option additionalItemsAnnotations (kotlin-spring): ;- or newline-separated annotations added to every collection element type. Unlike Java, @NotNull is not removed for nullable: true items. In Kotlin, element nullability is expressed by ?, which the compiler checks and Jackson 3's Kotlin module enforces on deserialization.
  • Nested collections work recursively: List<List<@Size(max = 3) kotlin.String>>.
  • Nullability is kept: List<@Valid Pet?>.
  • Annotations go before qualified names (@Size(max = 3) kotlin.String). Kotlin doesn't need Java's java.lang.@Size String placement.

Making it effective at runtime

  • By default the Kotlin compiler does not write type-use annotations to bytecode. All kotlin-spring Maven/Gradle templates (spring-boot, spring-cloud, spring-declarative-http-interface; SB2/3/4) now pass -Xemit-jvm-type-annotations when useBeanValidation=true. The option description tells users with their own build to add it.
  • Verified at runtime on the Spring Boot 3 sample: element constraints on model properties (including nested lists/sets) and on controller list parameters are enforced.
  • Response types carry no element annotations. With visible type annotations, ResponseEntity<List<@Valid Pet>> made every call fail with HV000197 (no ValueExtractor for ResponseEntity, see spring-projects/spring-framework#32375). Element annotations are therefore not added when rendering response types, so return signatures stay as they were before this PR.

Unchanged on purpose

  • Existing container-level annotations: @field:Valid on container properties, @get:Size for minItems/maxItems, @Valid @RequestBody / @RequestParam on parameters.
  • Reactive request bodies render Flow<{{{baseType}}}> and stay without element annotations.
  • Annotations on API parameters/request bodies beyond what dataType already produces are handled in #24780.

dataType audit

I checked the places where kotlin-spring uses dataType/returnType as something other than a type: default values ({{{dataType}}}("…") is scalar-only), @ApiResponse(implementation = X::class) (uses baseType), sealed/oneOf interfaces (baseType), x-kotlin-implements overrides (same types on both sides), the reactive x-reactive-array-string-return check, and enum-array branches. Inline enum arrays bypassed dataType, so I fixed them in dataClassReqVar/dataClassOptVar.

Imports

  • Models/APIs: existing javax/jakarta.validation.* wildcard imports already cover the new annotations.
  • delegate / service / serviceImpl / api_test: a new beanValidationImports.mustache partial imports only the annotations actually used on element types in that file. Operation x-extra-imports are added there when signatures contain custom element annotations.
  • x-*-message values are escaped as Kotlin string literals.

Docs

  • docs/generators/kotlin-spring.md: new additionalItemsAnnotations option.
  • x-field-extra-annotation description in VendorExtension now says it applies to the element type on items. Because the vendor extension description is shared, this regenerates one line in several Java/Spring/Kotlin generator docs.

Samples

  • New config bin/configs/kotlin-spring-boot-collection-item-annotations.yaml with its own annotated spec copy (petstore-collection-item-annotations.yaml), added to the samples-kotlin-server-jdk17 workflow matrix.
  • I regenerated all Kotlin configs. Only kotlin-spring samples changed: build files (compiler flag) and element-annotated types/imports.

Testing

  • ./mvnw -q -pl modules/openapi-generator test -Dtest='KotlinSpringServerCodegenTest' -Dsurefire.failIfNoSpecifiedTests=false -Djacoco.skip=true → passed (kotlin.spring.KotlinSpringServerCodegenTest: 309 tests, kotlin.KotlinSpringServerCodegenTest: 12 tests, 0 failures)
  • New tests cover $ref/oneOf/allOf/allOf:[$ref] items, all constraint annotations, uniqueItems sets, enum/free-form items, nullable items, nested arrays, x-field-extra-annotation (string/list), additionalItemsAnnotations alone and combined, useBeanValidation=false, response types without element annotations, imports in delegate/service files, and unchanged kotlin client output.
  • Samples compiled locally: the new collection sample plus delegate, reactive, swagger2, openApiNullable+modelMutable, spring-cloud, declarative-interface, SB2 (javax), SB3, SB4.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Makes bean validation actually apply to list/set element types in the Kotlin Spring server generator, so nested collection items are validated at runtime instead of only the collection itself.

  • Applies @Valid, @Size, @Pattern, @Email, @Min/@Max, and @DecimalMin/@DecimalMax from array items schemas to the element type (e.g. List<@Valid Pet>), plus x-field-extra-annotation and a new additionalItemsAnnotations option for element-type annotations.
  • Opt-in via no-op hooks in AbstractKotlinCodegen; only KotlinSpringServerCodegen overrides them, so other Kotlin generators are unchanged.
  • Generated Maven/Gradle builds pass -Xemit-jvm-type-annotations when useBeanValidation is enabled so the annotations reach bytecode, and element annotations are kept out of return types because Hibernate Validator can't extract values from ResponseEntity/Flow.
  • Regenerates Kotlin Spring samples and adds a collection-item-annotation sample config.