How to inject custom spring validation inside swagger codegen?
E

2

7

We are able to use openApi documentation and generate our Java Input classes using the spring swagger-codegen. Also, we can inject the javax.validation annotations when input is generated for common constraints like length, mandatory etc.

I would like to take this to the next customization level and be able to annotate the generated Input classes with custom validation annotations that are hooked up with @Constraint annotation from Spring. This way we can reuse specific validation for our project.

I'm hoping there is an out of the box solution for this. What is your preferred way of generating Input classes with custom validation annotations?

Enugu answered 30/8, 2019 at 13:41 Comment(1)
Did you found any well-looking way for it?Hustler
H
11

I did not find "out of the box solution". But openapi-generator provides a simple way of modifying generated code by editing mustache templates. That is how I solved exactly same problem as you have.

Basically I created custom field in OpenAPI specs where I specified custom constraint annotation (@EvenLong in my case). I called this field "x-constraints":

...
components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
          x-constraints: "@EvenLong"
        name:
          type: string
        tag:
          type: string
...

Then I told openapi-generator where to look for my custom/modified mustache templates. I used openapi-generator as maven plugin so I added templateDirectory property to plugin definition in pom.xml:

...
<plugin>
   <groupId>org.openapitools</groupId>
   <artifactId>openapi-generator-maven-plugin</artifactId>
   <version>4.3.1</version>
   <executions>
      <execution>
         <goals>
            <goal>generate</goal>
         </goals>
         <configuration>
            <inputSpec>
               ${project.basedir}/src/main/resources/openapi/specs/petstore.yaml
            </inputSpec>
            <templateDirectory>
              ${project.basedir}/src/main/resources/openapi/templates
            </templateDirectory>
            <generatorName>spring</generatorName>
            <apiPackage>sk.matusko.tutorial.openapicustomvalidations.api</apiPackage>
            <modelPackage>sk.matusko.tutorial.openapicustomvalidations.model</modelPackage>
            <configOptions>
               <interfaceOnly>true</interfaceOnly>
            </configOptions>
         </configuration>
      </execution>
   </executions>
</plugin>
...

and finally I edited 2 mustache templates so that my @EvenLong annotation ends up in output code.

What you do is copy needed files from https://github.com/OpenAPITools/openapi-generator/tree/v4.3.1/modules/openapi-generator/src/main/resources/JavaSpring to ${project.basedir}/src/main/resources/openapi/templates (or whatever directory you are using) and then add your changes to it.

First mustache template is beanValidationCore.mustache which renders content itself from x-constraints field.

I added {{ vendorExtensions.x-constraints }} so beanValidationCore.mustache looks like this

{{ vendorExtensions.x-constraints }}
{{#pattern}}@Pattern(regexp="{{{pattern}}}") {{/pattern}}{{!
minLength && maxLength set
}}{{#minLength}}{{#maxLength}}@Size(min={{minLength}},max={{maxLength}}) {{/maxLength}}{{/minLength}}{{!
minLength set, maxLength not
}}{{#minLength}}{{^maxLength}}@Size(min={{minLength}}) {{/maxLength}}{{/minLength}}{{!
minLength not set, maxLength set
}}{{^minLength}}{{#maxLength}}@Size(max={{maxLength}}) {{/maxLength}}{{/minLength}}{{!
@Size: minItems && maxItems set
}}{{#minItems}}{{#maxItems}}@Size(min={{minItems}},max={{maxItems}}) {{/maxItems}}{{/minItems}}{{!
@Size: minItems set, maxItems not
}}{{#minItems}}{{^maxItems}}@Size(min={{minItems}}) {{/maxItems}}{{/minItems}}{{!
@Size: minItems not set && maxItems set
}}{{^minItems}}{{#maxItems}}@Size(max={{maxItems}}) {{/maxItems}}{{/minItems}}{{!
@Email: useBeanValidation set && isEmail && java8 set
}}{{#useBeanValidation}}{{#isEmail}}{{#java8}}@javax.validation.constraints.Email{{/java8}}{{/isEmail}}{{/useBeanValidation}}{{!
@Email: performBeanValidation set && isEmail && not java8 set
}}{{#performBeanValidation}}{{#isEmail}}{{^java8}}@org.hibernate.validator.constraints.Email{{/java8}}{{/isEmail}}{{/performBeanValidation}}{{!
check for integer or long / all others=decimal type with @Decimal*
isInteger set
}}{{#isInteger}}{{#minimum}}@Min({{minimum}}){{/minimum}}{{#maximum}} @Max({{maximum}}) {{/maximum}}{{/isInteger}}{{!
isLong set
}}{{#isLong}}{{#minimum}}@Min({{minimum}}L){{/minimum}}{{#maximum}} @Max({{maximum}}L) {{/maximum}}{{/isLong}}{{!
Not Integer, not Long => we have a decimal value!
}}{{^isInteger}}{{^isLong}}{{#minimum}}@DecimalMin({{#exclusiveMinimum}}value={{/exclusiveMinimum}}"{{minimum}}"{{#exclusiveMinimum}},inclusive=false{{/exclusiveMinimum}}){{/minimum}}{{#maximum}} @DecimalMax({{#exclusiveMaximum}}value={{/exclusiveMaximum}}"{{maximum}}"{{#exclusiveMaximum}},inclusive=false{{/exclusiveMaximum}}) {{/maximum}}{{/isLong}}{{/isInteger}}

Second mustache template is model.mustache which renders java imports for java models generated from openapi specs. So I added import of all classes from my validators java package (where @EvenLong is) . Add import com.foo.bar.validators.*; to model.mustache. Mine looks like this:

package {{package}};

import java.util.Objects;
{{#imports}}import {{import}};
{{/imports}}
import org.openapitools.jackson.nullable.JsonNullable;
{{#serializableModel}}
import java.io.Serializable;
{{/serializableModel}}
{{#useBeanValidation}}
import javax.validation.Valid;
import com.foo.bar.validators.*;
import javax.validation.constraints.*;
{{/useBeanValidation}}
{{#performBeanValidation}}
import org.hibernate.validator.constraints.*;
{{/performBeanValidation}}
{{#jackson}}
{{#withXml}}
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
{{/withXml}}
{{/jackson}}
{{#withXml}}
import javax.xml.bind.annotation.*;
{{/withXml}}
{{^parent}}
{{#hateoas}}
import org.springframework.hateoas.RepresentationModel;
{{/hateoas}}
{{/parent}}

{{#models}}
{{#model}}
{{#isEnum}}
{{>enumOuterClass}}
{{/isEnum}}
{{^isEnum}}
{{>pojo}}
{{/isEnum}}
{{/model}}
{{/models}}

That's it!

Here is my detailed tutorial https://bartko-mat.medium.com/openapi-generator-to-spring-boot-with-custom-java-validations-623381df9215 and code samples https://github.com/Matusko/open-api-custom-validations

Heddi answered 5/1, 2021 at 17:23 Comment(3)
Life saver! I've tested it using kotlin-spring and the files changed a bit. You don't need to override all files from that directory. Even using Maven, it's not need to override all. Thanks! You should create tutorial about this.Troyes
Hi, thx for sharing. I think I implemented everything as you did, but I get an error when I try to build: pojo.mustache could not be found. But you didn't use the pojo, so I am confused... Another thing, in the api.mustache there's a note about it being auto generated and that it should not be edited manually. Do you see any risks there?Oviduct
This answer is three years old. I hope there's a newer version of OpenAPI been released that addresses this issue without needing to edit the Mustache templates! This is an awkward solution.Dorthea
D
1

Fortunately, as of OpenApiGenerator v6.x, we no longer need to customize mustache templates to do this. We can now use these two tags in the myProjectSpec.yaml file:

x-class-extra-annotation
x-field-extra-annotation

These are documented here

Here is an example:

schemas:
  MenuItemDto:
    type: object
    x-class-extra-annotation: "@com.foo.constraints.MyCustomConstraint"
    properties:
      id:
        type: integer
        format: int32
      name:
        type: string
        x-field-extra-annotation: "@NotBlank"
      itemPrice:
        type: number
        description: Price.
    required:
      - name
      - itemPrice

I could, of course, still modify the model.mustache file to add an import for the MyCustomConstraint annotation, but I'd rather not go that route, although it should work fine.

Dorthea answered 25/9 at 6:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.