annotation mapping & bean-validation

16. October 2022

The openapi-processor-spring/micronaut release 2022.5 adds a new annotation type mapping feature. It provides the possibility to add additional annotations to generated interfaces & classes.

annotation mapping

annotation type mapping allows to add annotations to a generated model class or to an endpoint method parameters of that class.

Let’s look at a contrived example to add a custom bean validation to the pojo model class of a schema.

See the annotation mapping documentation for more.

the example api

here is a simple api that takes a Foo schema as request body. The schema has some (useless ;-) number constraints on its properties.

openapi.yaml
openapi: 3.1.0
info:
  title: annotation mapping example
  version: 1.0.0

paths:
  /foo:
    post:
      summary: annotation mapping example endpoint.
      description: a simple endpoint where an annotation mapping is used on the request body
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Foo'
        required: true
      responses:
        '201':
          description: empty response

components:
  schemas:
    Foo:
      type: object
      properties:
        foo1:
          type: integer
          minimum: 0
        foo2:
          type: integer
          minimum: -10

bean validation annotations

Enabling bean validation in the mapping.yaml (the processor configuration) will generate a Foo class with bean validation annotations for the property constraints.

generated file
package io.openapiprocessor.openapi.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openapiprocessor.openapi.support.Generated;
import javax.validation.constraints.DecimalMin;

@Generated(value = "openapi-processor-spring", version = "2022.5")
public class Foo {

    @DecimalMin(value = "0") (1)
    @JsonProperty("foo1")
    private Integer foo1;

    @DecimalMin(value = "-10") (1)
    @JsonProperty("foo2")
    private Integer foo2;

    public Integer getFoo1() {
        return foo1;
    }

    public void setFoo1(Integer foo1) {
        this.foo1 = foo1;
    }

    public Integer getFoo2() {
        return foo2;
    }

    public void setFoo2(Integer foo2) {
        this.foo2 = foo2;
    }

}
1 the bean validation annotations created from the OpenAPI constraints.

custom bean validation annotation

Now we like to add a validation that checks the sum of the two Integer properties by writing @Sum(24).

Let’s create the annotation

manually created custom bean validation annotation
package io.openapiprocessor.samples.validations;


import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Constraint (validatedBy = {FooSumValidator.class})
@Target ({ ElementType.TYPE, ElementType.PARAMETER })
@Retention (value = RetentionPolicy.RUNTIME)
public @interface Sum {
    String message () default "invalid sum";
    Class<?>[] groups () default {};
    Class<? extends Payload>[] payload () default {};

    int value();
}

and the validation code.

manually created validation
package io.openapiprocessor.samples.validations;

import io.openapiprocessor.openapi.model.Foo;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class FooSumValidator implements ConstraintValidator<Sum, Foo> {
    private Integer sum;

    @Override
    public void initialize (Sum constraintAnnotation) {
        sum = constraintAnnotation.value ();
    }

    @Override
    public boolean isValid (Foo value, ConstraintValidatorContext context) {
        return value.getFoo1 () + value.getFoo2 () == sum;
    }
}

mapping for the custom annotation

Now comes the interesting part, the annotation type mapping that tells the processor to add our custom annotation to the generated Foo pojo model class.

processor configuration mapping.yaml
openapi-processor-mapping: v2.1 (1)

options:
  package-name: io.openapiprocessor.openapi
  bean-validation: true

map:
  types:
    (2)
    - type: Foo @ io.openapiprocessor.samples.validations.Sum(24)
1 the new mapping version. Using another version will produce a warning that the mapping is invalid.
2 the annotation mapping that tells the processor to @nnotate the Foo schema (Foo is the name of the OpenAPI schema) with the given annotation to the pojo model class generated for the Foo schema. The annotation is given with the fully qualified name (required to create the import) and (optionally) with fixed parameters.

model class with custom annotation

Now, with the annotation mapping in the configuration the processor will generate Foo like this:

generated file with custom annotation
package io.openapiprocessor.openapi.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openapiprocessor.openapi.support.Generated;
import javax.validation.constraints.DecimalMin;
import io.openapiprocessor.samples.validations.Sum;

@Sum(24) (1)
@Generated(value = "openapi-processor-spring", version = "2022.5")
public class Foo {

    @DecimalMin(value = "0")
    @JsonProperty("foo1")
    private Integer foo1;

    @DecimalMin(value = "-10")
    @JsonProperty("foo2")
    private Integer foo2;

    public Integer getFoo1() {
        return foo1;
    }

    public void setFoo1(Integer foo1) {
        this.foo1 = foo1;
    }

    public Integer getFoo2() {
        return foo2;
    }

    public void setFoo2(Integer foo2) {
        this.foo2 = foo2;
    }

}
1 our custom bean validation annotation.

summary

This little article described how to add a custom annotation to a generated class by adding an annotation type mapping to the processor mapping configuration.

To learn more about openapi-processor and how to generate controller interfaces and model classes from an OpenAPI description take a look at the documentation.