sa_vedem
sa_vedem

Reputation: 767

How to inject custom spring validation inside swagger codegen?

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?

Upvotes: 7

Views: 10815

Answers (2)

MiguelMunoz
MiguelMunoz

Reputation: 4972

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.

Upvotes: 2

Matúš Bartko
Matúš Bartko

Reputation: 2457

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

Upvotes: 11

Related Questions