Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Kotlin's kotlin-server generator does not support bean validation #12351

Closed
5 of 6 tasks
regispl opened this issue May 12, 2022 · 3 comments · Fixed by #14997
Closed
5 of 6 tasks

[BUG] Kotlin's kotlin-server generator does not support bean validation #12351

regispl opened this issue May 12, 2022 · 3 comments · Fixed by #14997

Comments

@regispl
Copy link

regispl commented May 12, 2022

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator (example)?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

The code generated for data classes for kotlin-server does not specify the annotation's use-site target, meaning that the bean validation can't pick it up due to the annotation generated for the Java elements being in the wrong place (it requires a get- or field-level annotation, while the default is the param one).

Context:
https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets
https://stackoverflow.com/questions/35847763/kotlin-data-class-bean-validation-jsr-303

E.g. with a spec containing a property like:

openapi: 3.0.3

info:
  title: FooBar
  version: 0.0.1

paths:
  /:
    get:
      responses:
          '200':
            description: Successful GET
    
components:
    
  schemas:
    attributes:
      type: array
      minItems: 1
      items:
        type: string

The limit constrain will be generated as:

data class FooBar (
    @JsonProperty("attributes")
    @Valid
    @NotNull
    @Size(min=1)    val attributes: kotlin.collections.List<kotlin.String>
)

It has to be generated as:

data class FooBar (
    @JsonProperty("attributes")
    @Valid
    @NotNull
    @get:Size(min=1)    val attributes: kotlin.collections.List<kotlin.String> // <--- See the "get:" here!
)

(this is a minimal change, although I think that Valid and JsonProperty should also get prefixed. NotNull is probably irrelevant, because Kotlin offers language constructs (?) that handle nullability.

It looks like the moustache template doesn't take this functionality into account, while the Spring Kotlin generator does.

openapi-generator version

5.4.0

OpenAPI declaration file content or url

See above.

Generation Details

generatorName = "kotlin-server" with "useBeanValidation" to "true", everything else is - I believe - irrelevant.

Steps to reproduce

Use the definition above API in a Kotlin project that supports bean validation.
I do not have a "minimal" project at hand to test it - I'm using my real one.

Related issues/PRs

None (AFAIK)

Suggest a fix

Replace all @<BeanValidationAnnotationName> with @get:<BeanValidationAnnotationName> or @field:<BeanValidationAnnotationName> in relevant Moustache templates, e.g.:

diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/beanValidationCore.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/beanValidat
ionCore.mustache
index d6e2f13b457..570903c1f85 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/beanValidationCore.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/beanValidationCore.mustache
@@ -1,20 +1,20 @@
-{{#pattern}} @Pattern(regexp="{{{.}}}"){{/pattern}}{{!
+{{#pattern}} @get:Pattern(regexp="{{{.}}}"){{/pattern}}{{!
 minLength && maxLength set
-}}{{#minLength}}{{#maxLength}} @Size(min={{minLength}},max={{maxLength}}){{/maxLength}}{{/minLength}}{{!
+}}{{#minLength}}{{#maxLength}} @get:Size(min={{minLength}},max={{maxLength}}){{/maxLength}}{{/minLength}}{{!
 minLength set, maxLength not

etc.

I have made the changes locally and can confirm they work, although I'm not sure if they're sufficient for all possible cases etc. - they work for what I need them to work and what I can easily test. I can open a PR if that's desired.

@dmivankov
Copy link

Extra details is that @Size/@Min/@Max/... annotations end up being applied to constructor arguments which it seems aren't bean-validated (multiple constructors can be present, so post-creation validation can't know which one was used)

javap -p -v can show where annotations end up: look up annotation name in Constant pool:, look by number in RuntimeVisibleAnnotations: or RuntimeVisibleParameterAnnotations:, moving to getter or field level makes validator see them

@dmivankov
Copy link

dmivankov commented Oct 28, 2022

more cautious/compatible option could be to apply both: @param:Size + @get:Size
https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets

@oalyman
Copy link

oalyman commented Mar 16, 2023

I stumbled across this issue and needed a quick pragmatic fix.
So in my case a good enough solution for now was to modify the openApiGenerate task to replace the wrong annotation form in the source files after code generation.
Maybe this is of value for anyone as long as this ticket is still open

Given a gradle build with kotlin this looks like:

tasks.openApiGenerate {
    doLast {
        // pragmatic fix for https://github.com/OpenAPITools/openapi-generator/issues/12351
        val replaceAnnotations = setOf(
            "Valid",
            "AssertFalse",
            "AssertTrue",
            "DecimalMax",
            "DecimalMin",
            "Digits",
            "Email",
            "Future",
            "FutureOrPresent",
            "Max",
            "Min",
            "Negative",
            "NegativeOrZero",
            "NotBlank",
            "NotEmpty",
            "NotNull",
            "Null",
            "Past",
            "PastOrPresent",
            "Pattern",
            "Positive",
            "PositiveOrZero",
            "Size"
        )
        val kotlinFiles = File("$projectDir/<path_to_your_model_directory>").listFiles()
        kotlinFiles.forEach { file ->
            val lines = file.useLines { it.toList() }
            val newLines = lines.map {
                replaceAnnotations.fold(it) { acc, annotation ->
                    acc.replace("@$annotation", "@get:$annotation")
                }
            }

            file.writeText(newLines.joinToString("\n"))
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants