DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Automated Bug Fixing: From Templates to AI Agents
  • Publishing Flutter Packages to JFrog Artifactory
  • Dynamic File Upload Component in Salesforce LWC
  • Simplifying Multi-Cloud Observability With Open Source

Trending

  • Understanding Java Signals
  • Solid Testing Strategies for Salesforce Releases
  • Ensuring Configuration Consistency Across Global Data Centers
  • Grafana Loki Fundamentals and Architecture
  1. DZone
  2. Software Design and Architecture
  3. Integration
  4. OpenAPI: Extend Functionality of Generator Plugin Using Custom Mustache Templates

OpenAPI: Extend Functionality of Generator Plugin Using Custom Mustache Templates

This review is focused on the basic concepts of the OpenAPI generator plugin and has some tips on how to use mustache templates.

By 
K.D. Prog user avatar
K.D. Prog
·
Dec. 18, 23 · Review
Likes (1)
Comment
Save
Tweet
Share
6.1K Views

Join the DZone community and get the full member experience.

Join For Free

OpenAPI Specification is a standard for documentation of HTTP REST services. You can try it using the Swagger editor. Based on specification files, code generators generate all model, server, and client classes required for development. This article is about the OpenAPI generator. It allows the generation of API client libraries (SDK generation), server stubs, documentation, and configuration automatically given an OpenAPI Spec. It’s available for several programming languages. I will use Java 17 and the OpenAPI generator of the 6.0.0 version to give an example of how to extend its functionality by amending mustache templates used by this code generator.

Quick Guide to Mustache

Before going further, let’s take a closer look at the mustache itself. It will help us amend current OpenAPI templates according to our needs. 

Mustache calls itself a logic-less template. It is logical-less because it does not support cycles and if-then statements. It uses tags that you can replace with values from the context. By default, all tags use HTML escape when replacing. However, we can bypass this default behavior. You can find the detailed guide here.

So, a mustache might be very helpful when you need to generate some text with your own substitutions where it’s necessary. After that, you can use it as you wish. For example, you can create files with .java extension and compile them.

Here is a simple mustache template test.mustache with common cases.

test.mustache

Plain Text
 
 !!!!!Print variable!!!!!
{{#title}} {{! add 'print title %title value%' to the output if title exists in context}}
{{=<% %>=}} <%! change delimiter to <% %>
print title <%title%> <%! here we substitute variable 'title' with its value from context%>
<%={{ }}=%> {{! change delimiter back to default}}
{{/title}}
{{^title}} {{! add 'no title ' to the output if title not exists in context}}
no title
{{/title}}
{{> templates/test-list.mustache }} {{! Include test-list.mustache file}}
{{> templates/show-context.mustache }} {{! Include show-context.mustache file}}


If you execute this template, providing all the required data, you will get something like this.

Plain Text
 
!!!!!Print variable!!!!!
 
 
print title mustache example 
 
!!!!!Print list!!!!!
 
Let's show how to check variable existence
 
Let's show how lists are processed 
 
 
!!!!!Print context!!!!!
{todos=[{message=show how to check variable existence}, {message=show how lists are processed }], title=mustache example} 
{todos=[{message=show how to check variable existence}, {message=show how lists are processed }], title=mustache example}  


So, the output is some text with substitutions from the context printed in that latest line.

Plain Text
 
{todos=[{message=show how to check variable existence}, {message=show how lists are processed }], title=mustache example}  


Let’s go through some details. 

Mustache uses tags. 

To distinguish variables that we want to replace from the other text, it uses a tag {{  }} by default. So, for example, when you write {{title}}, you mean it’s a variable title you want to substitute. The default tag can be changed using the tag {{=  =}}. It’s very similar to the first one, except for the equal sign. For example, {{=<% %>=}} will change the tag for variables to <% %>, so a title variable in this case will look like this <%title%>. To change it back, use the same approach <%={{ }}=%>.

You can also include other templates in the current template. Use {{> }} tag. For example, {{> test-list.mustache}}. It will put the contents of test-list.mustache file in test.mustache from the place, it is stated. Take a look at test-list.mustache 

test-list.mustache

Plain Text
 
!!!!!Print list!!!!!
{{#todos}} {{! print the list of values from context variable 'todos'}}
Let's {{message}}
{{/todos}} {{! if no 'todos' variable exists in context or this list is empty}}
{{^todos}}
No todos!
{{/todos}}


It provided the output that you already saw above.

Plain Text
 
!!!!!Print list!!!!!

Let's show how to check variable existence

Let's show how lists are processed


As you can guess from the name of the template, it substitutes the list of variables in your template. To start writing lists {{# }} tag is used. To finish writing {{/ }} is used. The substitution for text Let's {{message}} will be implemented as many times as the number of elements in the variable todos. According to context:

Plain Text
 
{title=mustache example, todos=[{message=show how to check variable existence}, {message=show how lists are processed }]}


There are two elements message=show how to check variable existence and message=show how lists are processed.

If todos variable is null or empty nothing will be added to the output.  This situation can also be handled using the tag {{^ }}. It works like if (todos == null || todos.size() == 0), so when  todos variable is null or empty, you can add any text you need. The handler finishes with the tag {{/ }}. For example:

Plain Text
 
{{^todos}}
No todos!
{{/todos}}


This template will print No todos! if there is no todos variable in context or if this variable is an empty list.

The same tags {{# }}, {{^ }} can be used to check if a variable is not null. For example,

Plain Text
 
{{#title}}
print title ‘{{title}}’
{{/title}}
{{^title}}
no title
{{/title}}


If title = “variable exists” this template will provide the output “print title ‘variable exists’.”

If title is null or is not part of the context, the output will be “no title.”

I always say context in all previous examples. So, what is this? It’s the data that is used for substitutions. Mustache provides a special tag {{.}} that substitutes all the data that was passed for this template for replacements. Here is the template below that prints the context.

show-context.mustache

Plain Text
 
!!!!!Print context!!!!!
{{.}} {{! add all context to the output}}
{{{.}}} {{! add all context to the output without HTML codes}}


{{.}} prints data with HTML codes and {{{.}}} substitutes data as is. Here is the output of the template.

Plain Text
 
!!!!!Print context!!!!!

{title=mustache example, todos=[{message=show how to check variable existence}, {message=show how lists are processed }]} 

{title=mustache example, todos=[{message=show how to check variable existence}, {message=show how lists are processed }]}


I didn't use a code block to show the difference. In this example, HTML escape is applied for the character ‘=’. HTML escapes work in the same way as tag {{ }}. For example, {{title}} with value key=test will provide output key&#x3D;test and {{{title}}} for the same value will return key=test. &#x3D; is a hex code and &#61; is decimal HTML code for symbol ‘=’.

If you want to try mustache by yourself and run the templates above, you can add the following dependency to your project.

Java
 
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>jmustache</artifactId>
<version>1.14</version>
</dependency>


I used the same library that OpenAPI uses in the 6.0.0 version. To execute a template, you need an instance of com.samskivert.mustache.Template. Here is a code snippet.

Java
 
import com.samskivert.mustache.Mustache;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Map;

public class MustacheCompileService {
    private final Mustache.Compiler compiler;

    public MustacheCompileService() {
        this.compiler = Mustache.compiler();
    }
  
    public String compileTemplate(final Map<String, Object> bundle, final String template) {
        final var tmpl = compiler
                .withLoader(
                        this::getTemplate
                )
                .compile(getTemplate(template));
        return tmpl.execute(bundle);
    }
  
    private Reader getTemplate(final String name) {
        return new InputStreamReader(                
          Thread.currentThread().getContextClassLoader().getResourceAsStream(name)
        );
    }
}


Put your templates in the classpath. I put them in src\main\resources\templates. I used java.util.Map as context. You can also use any POJO.

Here are some examples of the output.

Java
 
System.out.println(
        compiler.compileTemplate(
                Map.of(
                        "title", "mustache example",
                        "todos",
                        List.of(
                                Map.of("message", "show how to check variable existence"),
                                Map.of("message", "show how lists are processed ")
                        )
                ),
                "templates/test.mustache"
        )
);

!!!!!Print variable!!!!!
 
 
print title mustache example 
 
!!!!!Print list!!!!!
 
Let's show how=to check variable existence
 
Let's show how lists are processed 
 
 
!!!!!Print context!!!!!
{todos=[{message=show how=to check variable existence}, {message=show how lists are processed }], title=mustache example} 
{todos=[{message=show how=to check variable existence}, {message=show how lists are processed }], title=mustache example}  
Java
 
System.out.println(
        compiler.compileTemplate(
                Map.of(),
                "templates/test.mustache"
        )
);


!!!!!Print variable!!!!!
 
no title
!!!!!Print list!!!!!
 
No todos!
 
!!!!!Print context!!!!!
{} 
{}


So, in the last example with empty context, you can ensure that all handlers for null and empty variables work correctly. 

Usage of Mustache in OpenAPI Generator

I used version 6.0.0 of the OpenAPI generator in my examples. This version uses com.samskivert:jmustache:1.14. You can find the code here.

The usage of mustache you can find in class org.openapitools.codegen.templating.MustacheEngineAdapter.

Java
 
public String compileTemplate(TemplatingExecutor executor, Map<String, Object> bundle, String templateFile) throws IOException {
    Template tmpl = compiler
            .withLoader(name -> findTemplate(executor, name))
            .defaultValue("")
            .compile(executor.getFullTemplateContents(templateFile));
    return tmpl.execute(bundle);
}


This method is very similar to the one I used above. So, all the things described previously work correctly here as well.

Templates are in src\main\resources. For example, src\main\resources\Java, src\main\resources\JavaSpring.  As you can guess from the names of directories, there are templates of classes for Java and String frameworks. After reading your .yaml and .json specification files with HTTP server methods descriptions OpenAPI generator generates required .java classes from these templates, substituting required variables with the values from your .yaml and .json files.

Further, I’m going to use org.openapitools:openapi-generator-maven-plugin plugin for Maven that implements OpenAPI code generation.

Custom Validation in Generated HTTP Server Methods

It’s a very common task to validate the input and output parameters of your HTTP methods. Some validations are simple; for example, it’s enough to check that the value is not null. However, you might have requirements to make more complex validations.

Hibernate validator based on Javax (Jakarta) validation API is one of the most suitable tools for such tasks. More details about the hibernate validator are here.

You can switch it on for the 6.0.0 version of the OpenAPI generator with vendorExtensions.x-constraints. Extensions allow you to provide vendor-specific configurations to your specification document. You can read about vendor extensions here.

So, I’m going to use vendorExtensions.x-constraints to generate code with Javax(Jakarta) validation annotations and custom annotations for HTTP method parameters. And here, we need to amend the default OpenAPI generator mustache template. 

But first of all, let’s create a model to test our changes.

Generate Commons and Model

I am going to generate an HTTP method that should create an account report. The result of this method is a file with a report. All generated code is for Spring Framework.

Finally, OpenAPI will generate a Spring controller with request and response.  I assume that there will be more methods in this specification later, so I will devote some common data to it. Besides, I will generate it before the server code is used vendorExtensions.x-constraints properly. Later I will explain why I did it this way and it will be up to you to choose another way.

So, here are the commons.

common.yaml

YAML
 
openapi: 3.0.1
info:
  title: API example documentation.
  version: 0.1.0
paths: { }
components:
  schemas:
    AgreementType:
      nullable: true
      description: agreement type.
      type: string
      enum:
        - 'loan'
        - 'card'
      x-enum-varnames:
        - LOAN
        - CARD
    CardType:
      description: card type.
      type: string
      nullable: true
      enum:
        - 'creditCard'
        - 'debitCard'
      x-enum-varnames:
        - CREDIT
        - DEBIT
    AccountParameter:
      type: object
      required:
        - name
      properties:
        name:
          $ref: '#/components/schemas/Parameters'
        value:
          description: parameter value
          type: string
    Parameters:
      type: string
      nullable: true
      x-constraints: "/*not null parameter name*/@NotNull(message = \"{notnull.account.param-name}\")"
      enum:
        - 'siebelId'
        - 'processingId'
        - 'status'
        - 'id'
      x-enum-varnames:
        - SIEBEL_ID
        - PROCESSING_ID
        - STATUS
        - ID


Almost all these classes are enums.

Here is the request response model.

account.yaml

YAML
 
GetReportAccountRequest:
  type: object
  required:
    - payload
  properties:
    payload:
      $ref: '#/AccountReportData'
GetReportAccountResponse:
  type: object
  properties:
    file:
      description: отчет.
      type: string
AccountReportData:
  x-constraints: "/*id, status, siebelId params are mandatory*/@org.example.validation.AccountParametersValidation"
  type: object
  required:
    - agreementNumber
    - agreementType
    - accountNumber
    - date
  properties:
    agreementNumber:
      description: agreement number.
      type: string
      x-constraints: "/*not empty value*/@NotBlank(message = \"{notnull.account.agreement}\")"
    agreementType:
      description: agreement type.
      type: AgreementType
      x-constraints: "/*not null value*/@NotNull(message = \"{notnull.account.agreement.type}\")"
    cardType:
      description: card type.
      type: CardType
    accountNumber:
      description: account number.
      type: string
      x-constraints: "/*not empty value*/@NotBlank(message = \"{notnull.account.number}\")"
    date:
      description: account date.
      type: string
      format: date
      x-constraints: "/*not null value*/@NotNull(message = \"{notnull.account.date}\")"
    parameters:
      description: >
        Parameters:
          - id
            - GUID
            - mandatory, unique
            - unique identifier.
          - siebelId
            - mandatory
            - client siebel identifier
          - processingId
            - unique process identifier
          - status
            - mandatory
            - process status. Possible values:
              in progress
              completes
              interrupted
              blocked
      type: array
      items:
        type: AccountParameter


The request contains some fields like accountNumber and list of parameters. Some parameters from this list are mandatory, and we have to verify it. I created a custom validator for this constraint and annotation @AccountParametersValidation.

All validations in this model are added using the tag x-constraints. I simply put the required annotation and comments. OpenAPI will add these annotations to the required fields when generating. That’s all I need. When executing this code, Spring will run the corresponding validator. 

The common data type is used here using the tag type, for example type:AgreementType. I will show later the OpenaAPI generator maven config where I put all type mappings. If I use a tag $ref for agreemnetType field, I would have to put x-constraints in definition of AgreementType, for example:

YAML
 
AgreementType:
  x-constraints: "/*not null value*/@NotNull(message = \"{notnull.account.agreement.type}\")"
  description: agreement type.
  type: string
  enum:
    - 'loan'
    - 'card'
  x-enum-varnames:
    - LOAN
    - CARD


Because $ref works by replacing itself and everything on its level with the definition it is pointing at. So, all other tags placed together  $ref will be ignored, including x-constraints. However, I assume there will be methods in my Spring controller where agreemnetType the field is not mandatory, and I should have the ability to add validation where I need to.

Before going further, I will show the rest of the API documentation files and custom validator implementation for @AccountParametersValidation annotation.

Here is the HTTP method for account report generation:

account.api.yaml

YAML
 
generate:
  post:
    tags:
      - Report
    summary: Generate account report
    operationId: generate
    requestBody:
      description: Request body of account report
      required: true
      content:
        application/json:
          schema:
            $ref: './components/account/account.yaml#/GetReportAccountRequest'
    responses:
      200:
        description: Ответ
        content:
          application/json:
            schema:
              $ref: './components/account/account.yaml#/GetReportAccountResponse'
      401:
        description: authorisation error
      403:
        description: access forbidden
      404:
        description: not found
      500:
        description: internal server error


And the final API documentation file for the OpenAPI generator.

api.yaml

YAML
 
openapi: 3.0.1
info:
  title: api documentation
  description: API example documentation
  version: 0.1.0
tags:
  - name: Report
    description: generate report service
paths:
  /v1/account:
    $ref: './account.api.yaml#/generate'


If you like, you can put the contents of account.api.yaml, account.yaml in api.yaml file.

Custom Validator Implementation

Here is @AccountParametersValidation annotation.

AccountParametersValidation.class

Java
 
package org.example.validation;

import org.example.server.api.model.GetReportAccountRequestHttp;
import org.example.validation.validator.AccountParametersValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Custom validation of {@link GetReportAccountRequestHttp}.
 */
@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Constraint(validatedBy = AccountParametersValidator.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccountParametersValidation {
    /**
     * Validation error message.
     *
     * @return message
     */
    String message() default "mandatory fields error";

    /**
     * Validation groups.
     *
     * @return groups
     */
    Class<?>[] groups() default {};

    /**
     * Metadata.
     *
     * @return payload
     */
    Class<? extends Payload>[] payload() default {};
}


It requires AccountParametersValidator.class. Here it is.

AccountParametersValidator.class

Java
 
package org.example.validation.validator;

import org.example.common.model.AccountParameter;
import org.example.common.model.Parameters;
import org.example.server.api.model.AccountReportDataHttp;
import org.example.validation.AccountParametersValidation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.isBlank;

/**
 * Custom validator for {@link AccountParametersValidation}.
 */
public class AccountParametersValidator
    implements ConstraintValidator<AccountParametersValidation, AccountReportDataHttp> {

    @Override
    public boolean isValid(final AccountReportDataHttp payload, final ConstraintValidatorContext context) {
        boolean hasNoChanges = true;
        final var params = getParams(payload == null ? null : payload.getParameters());
        if (isBlank(params.get(Parameters.ID))) {
            addConstraintViolation("notnull.account.param.id", context);
            hasNoChanges = false;
        }
        if (isBlank(params.get(Parameters.STATUS))) {
            addConstraintViolation("notnull.account.param.status", context);
            hasNoChanges = false;
        }
        if (isBlank(params.get(Parameters.SIEBEL_ID))) {
            addConstraintViolation("notnull.account.param.siebel.id", context);
            hasNoChanges = false;
        }
        return hasNoChanges;
    }

    void addConstraintViolation(String code, ConstraintValidatorContext context) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate("{" + code + "}").addConstraintViolation();
    }

    private Map<Parameters, String> getParams(final List<AccountParameter> parameters) {
        return defaultIfNull(parameters, Collections.<AccountParameter>emptyList()).stream()
                .filter(param -> param != null && Objects.nonNull(param.getName()))
                .collect(
                        Collectors.toMap(
                                AccountParameter::getName,
                                AccountParameter::getValue,
                                (oldValue, newValue) -> newValue,
                                HashMap::new
                        )
                );
    }
}


This class converts the parameter list to a map where the key is the parameter name, and the value is the parameter value and verifies that the values of mandatory parameters are not null or empty. All errors are put in ConstraintValidatorContext.

So, now all preparations are finished, and it’s time to amend the default OpenAPI mustache templates to make x-constraints tag works properly to add annotations from this tag to the generated code.

Customize Default OpenAPI Mustache Templates

I created a templates directory in the root folder of my project. I am going to put all my templates in this directory.

First of all, let’s enable vendorExtensions.x-constraints. We need to amend beanValidationCore.mustache template. You can take the actual template for the required version here. I use version 6.0.0 to generate Spring controllers, so the template I need is here.

To enable vendorExtensions.x-constraints we need to add tag {{{ vendorExtensions.x-constraints }}}.

beanValidationCore.mustache

Plain Text
 
{{{ vendorExtensions.x-constraints }}}
{{#pattern}}{{^isByteArray}}@Pattern(regexp = "{{{pattern}}}") {{/isByteArray}}{{/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}}{{/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}}{{/minItems}}{{!
@Email: useBeanValidation set && isEmail set
}}{{#useBeanValidation}}{{#isEmail}}@Email{{/isEmail}}{{/useBeanValidation}}{{!
check for integer or long / all others=decimal type with @Decimal*
isInteger set
}}{{#isInteger}}{{#minimum}}@Min({{.}}) {{/minimum}}{{#maximum}}@Max({{.}}) {{/maximum}}{{/isInteger}}{{!
isLong set
}}{{#isLong}}{{#minimum}}@Min({{.}}L) {{/minimum}}{{#maximum}}@Max({{.}}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}}


So, it’s done. vendorExtensions.x-constraints is enabled. This is the way vendorExtensions are enabled. 

Maybe you noticed that in account.yaml I used the canonical name @org.example.validation.AccountParametersValidation. I did it so as not to add imports in generated model classes. However, we can do it. Model classes are generated based on model.mustache template, so let’s add the required imports. The default template for my version is here.

model.mustache

Plain Text
 
package {{package}};
import java.net.URI;
import java.util.Objects;
{{#imports}}import {{import}};
{{/imports}}
{{#openApiNullable}}
import org.openapitools.jackson.nullable.JsonNullable;
{{/openApiNullable}}
{{#serializableModel}}
import java.io.Serializable;
{{/serializableModel}}
import java.time.OffsetDateTime;
{{#useBeanValidation}}
import org.example.validation.AccountParametersValidation;
{{#useJakartaEe}}
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
{{/useJakartaEe}}
{{^useJakartaEe}}
import javax.validation.Valid;
import javax.validation.constraints.*;
{{/useJakartaEe}}
{{/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}}
{{#swagger2AnnotationLibrary}}
import io.swagger.v3.oas.annotations.media.Schema;
{{/swagger2AnnotationLibrary}}
{{#withXml}}
import javax.xml.bind.annotation.*;
{{/withXml}}
{{^parent}}
{{#hateoas}}
import org.springframework.hateoas.RepresentationModel;
{{/hateoas}}
{{/parent}}
import java.util.*;
{{#useJakartaEe}}
import jakarta.annotation.Generated;
{{/useJakartaEe}}
{{^useJakartaEe}}
import javax.annotation.Generated;
{{/useJakartaEe}}
{{#models}}
{{#model}}
{{#isEnum}}
{{>enumOuterClass}}
{{/isEnum}}
{{^isEnum}}
{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>pojo}}{{/vendorExtensions.x-is-one-of-interface}}
{{/isEnum}}
{{/model}}
{{/models}}


If you pay attention, you notice that there is a special variable useBeanValidation that can be used to enable/disable validation. You can also find imports for javax.validation.Valid and javax.validation.constraints.*. If useJakartaEe variable is enabled, corresponding imports for Jakarta are used.

There is one more thing I’m going to change. It’s about required property. For example,

YAML
 
Data:
  type: object
  required:
    - name
  properties:
    name:
      type: string
    value:
      type: string


When you generate code for the Data object defined above OpenAPI will add @javax.validation.constraints.NotNull annotation for getName() method. However, I’m adding my own validation annotations using x-constraints with my custom error message, like

YAML
 
Data:
  type: object
  required:
    - name
  properties:
    name:
      type: string
      x-constraints: "@NotBlank(message = \"{notnull.name}\")"
    value:
      type: string


It turns out that OpenAPI will generate both @NotNull, because I set the name as required, and my @NotBlank(message = "{notnull.name}"). But I don’t need both; I need only @NotBlank(message = "{notnull.name}").

Well, you say let’s simply remove required for name filed, and it’s done. It works; however, if we make it this way, we miss the required flag in the documentation.

name field

As you can see name field is marked as required, and some people strongly ask to keep it this way.

And we can do it. I mean both our custom validation and required flag. All we need is to change beanValidation.mustache template. The default one for 6.0.0 is here. I changed it respectively.

beanValidation.mustache

Plain Text
 
{{#required}}
{{^isReadOnly}}
{{^vendorExtensions.x-constraints}}
@NotNull
{{/vendorExtensions.x-constraints}}
{{/isReadOnly}}
{{/required}}
{{#isContainer}}
{{^isPrimitiveType}}
{{^isEnum}}
@Valid
{{/isEnum}}
{{/isPrimitiveType}}
{{/isContainer}}
{{^isContainer}}
{{^isPrimitiveType}}
@Valid
{{/isPrimitiveType}}
{{/isContainer}}
{{>beanValidationCore}}


So, as you remember previously, in the section ‘Quick guide to mustache,’ we already discussed how to check if the specific variable is in the mustache context and how to add handlers for cases when it’s not there. In this case, I verify that if vendorExtensions.x-constraints is enabled do not add @NotNull annotation for fields marked as required. Meantime, the documentation will look the same:

documentation

Now it’s done.

So, we took some default OpenAPI mustache templates and changed them according to our needs.

The only thing left is to configure the generator plugin. As I mentioned before, I used the Maven project, so I have to configure the OpenAPI generator Maven plugin. 

OpenAPI Generator Maven Plugin Configuration

The description and tutorial can be found here. This tutorial is for version 6.0.0, but you can choose any version you need. Some options are described here.

First of all, let’s generate common classes described in common.yaml. Here is the config.

Java
 
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.0.0</version>
<executions>
    <execution>
        <id>generate-common</id>
        <goals>
            <goal>generate</goal>
        </goals>
        <configuration>
            <inputSpec>
                ${project.basedir}/common.yaml
            </inputSpec>
            <generatorName>spring</generatorName>
            <modelPackage>org.example.common.model</modelPackage>
            <generateApis>false</generateApis>
            <generateSupportingFiles>false</generateSupportingFiles>
            <configOptions>
                <openApiNullable>false</openApiNullable>
                <additionalModelTypeAnnotations>
                    @lombok.Builder
                    @lombok.NoArgsConstructor
                    @lombok.AllArgsConstructor
                </additionalModelTypeAnnotations>
            </configOptions>
            <templateDirectory>
                ${project.basedir}/templates
            </templateDirectory>
        </configuration>
    </execution>
</executions>
</plugin>


According to this config, I’m going to generate model classes in the package org.example.common.model from ${project.basedir}/common.yaml. As was previously discussed, I decided to generate code for Spring Framework; that’s why generatorName=spring is used, and it will use templates from src\main\resources\JavaSpring. To generate commons, I could use java generator as well; however, it may require additional dependencies in pom.xml, so I keep spring generator for commons as well. 

As soon as I need to generate only the model generateApis=false. 

generateSupportingFiles=false to avoid generating unnecessary classes.

openApiNullable=false to avoid generating code with the usage of jackson-databind-nullable library. This library will generate JsonNullable objects for all properties marked as nullable: true.

In common.yaml I marked enums with this flag. So, if you discover this template, you will find a code: 

Java
 
@JsonCreator
    public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) {
      for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) {
        if (b.value.equals(value)) {
          return b;
        }
      }
      {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/isNullable}}
    }


nullable value is isNullable variable value, so in case if nullable=true @JsonCreator will return null if there is an incorrect enum value. This is exactly what I need because I use my own validation, and if I need the field of such an enum type to be not null, I will add the required annotation using x-constraints with an appropriate error message.

I also added @lombok.Builder, @lombok.NoArgsConstructor, @lombok.AllArgsConstructor lombok annotations to model classes to generate corresponding constructors and methods. This might be useful in converters or other application code. You can read about lombok here.

And finally templateDirectory points to the directory with custom mustache templates. It is ${project.basedir}/templates in my case.

If you run, mvn clean compile you get generated code in target/generated-sources/openapi/src/main/java/org/example/common/model. There should be 4 Java classes. 

So, everything is ready to generate Spring controllers with requests and responses. My HTTP method generate is described in api.yaml. Here is the Maven plugin configuration.

Java
 
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.0.0</version>
<executions>
    <execution>
        <id>generate-common</id>
        <goals>
            <goal>generate</goal>
        </goals>
        <configuration>
            <inputSpec>
                ${project.basedir}/common.yaml
            </inputSpec>
            <generatorName>spring</generatorName>
            <modelPackage>org.example.common.model</modelPackage>
            <generateApis>false</generateApis>
            <generateSupportingFiles>false</generateSupportingFiles>
            <configOptions>
                <openApiNullable>false</openApiNullable>
                <additionalModelTypeAnnotations>
                    @lombok.Builder
                    @lombok.NoArgsConstructor
                    @lombok.AllArgsConstructor
                </additionalModelTypeAnnotations>
            </configOptions>
            <templateDirectory>
                ${project.basedir}/templates
            </templateDirectory>
        </configuration>
    </execution>
    <execution>
        <id>generate-server</id>
        <goals>
            <goal>generate</goal>
        </goals>
        <configuration>
            <inputSpec>
                ${project.basedir}/api.yaml
            </inputSpec>
            <generatorName>spring</generatorName>
            <apiPackage>org.example.server.api</apiPackage>
            <modelPackage>org.example.server.api.model</modelPackage>
            <generateApis>true</generateApis>
            <generateSupportingFiles>false</generateSupportingFiles>
            <languageSpecificPrimitives>CardType,AgreementType,AccountParameter</languageSpecificPrimitives>
            <typeMappings>
                <typeMapping>CardType=org.example.common.model.CardType</typeMapping>
                <typeMapping>AgreementType=org.example.common.model.AgreementType</typeMapping>
                <typeMapping>AccountParameter=org.example.common.model.AccountParameter</typeMapping>
            </typeMappings>
            <configOptions>
                <openApiNullable>false</openApiNullable>
                <useTags>true</useTags>
                <interfaceOnly>true</interfaceOnly>
                <skipDefaultInterface>true</skipDefaultInterface>
                <additionalModelTypeAnnotations>
                    @lombok.Builder
                    @lombok.NoArgsConstructor
                    @lombok.AllArgsConstructor
                </additionalModelTypeAnnotations>
            </configOptions>
            <additionalProperties>
                <additionalProperty>modelNameSuffix=Http</additionalProperty>
            </additionalProperties>
            <templateDirectory>
                ${project.basedir}/templates
            </templateDirectory>
        </configuration>
    </execution>
</executions>
</plugin>


I added a task generate-server to the already existing openapi-generator-maven-plugin configuration. All definitions come from ${project.basedir}/api.yaml file. generatorName is still spring. But this time generateApis=true and String controllers are generated in org.example.server.api package. Requests and responses will be in org.example.server.api.model package. Besides, all requests and responses will get postfix Http, for example AccountReportDataHttp.java, as it’s configured within the tag additionalProperty with the property name modelNameSuffix.   

generateSupportingFiles is still false to avoid generating examples and other classes; however, at this time, we have to add skipDefaultInterface=true to skip generation of default implementation for generated interfaces that use classes we skipped by setting generateSupportingFiles to false.

interfaceOnly=true says to generate only interfaces for Spring controllers.  I will add implementation by myself.

useTags=true will generate @io.swagger.v3.oas.annotations.tags.Tag for controller interfaces. It helps to group several HTTP methods in OpenAPI documentation. For example,

report

corresponds to @Tag(name = "Report", description = "generate report service")

Pay attention to typeMappings tag. We need these mappings to use classes generated by generate-common task when generating ReportApi Spring controller. Let’s take a look at account.yaml, for example

YAML
 
cardType:
  description: card type.
  type: CardType


How OpenAPI generator plugin understand that CardType is org.example.common.model.CardType? It uses typeMappings. All custom types are mentioned in languageSpecificPrimitives and their mappings are in typeMappings.

With all other settings, you are already familiar.

If you run mvn clean compile you get generated code in target/generated-sources/openapi/src/main/java. At this time, besides classes in org/example/common/model , there will be one Java interface ReportApi.java in org/example/server/api and three Java classes in org/example/server/api/model. Everything is as we configured.

Before testing generated code, I think it might be useful to provide all dependencies I used in my pom.xml.

Java
 
<properties>
    <java.version>17</java.version>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <lombok.version>1.18.28</lombok.version>
    <findbugs.annotations.version>3.0.1</findbugs.annotations.version>
    <jackson-version>2.11.2</jackson-version>
    <swagger-annotations.version>1.6.2</swagger-annotations.version>
    <javax-validation.version>1.1.0.Final</javax-validation.version>
    <spring-version>5.3.18</spring-version>
    <jackson-version>2.11.2</jackson-version>
    <javax-servlet.version>4.0.1</javax-servlet.version>
    <javax-annotation.version>1.3.2</javax-annotation.version>
    <javax-validation.version>2.0.1.Final</javax-validation.version>
    <hibernate.validator.version>6.2.3.Final</hibernate.validator.version>
    <springfox.core.version>3.0.0</springfox.core.version>
    <findbugs.version>3.0.1</findbugs.version>
    <swagger.annotations.version>2.2.0</swagger.annotations.version>
    <commons.lang3.version>3.12.0</commons.lang3.version>
    <openapi-generator-plugin.version>6.0.0</openapi-generator-plugin.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>${commons.lang3.version}</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    <dependency>
        <groupId>io.swagger.core.v3</groupId>
        <artifactId>swagger-annotations</artifactId>
        <version>${swagger.annotations.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>${javax-servlet.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>${javax-validation.version}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>${hibernate.validator.version}</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-core</artifactId>
        <version>${springfox.core.version}</version>
    </dependency>
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>${javax-annotation.version}</version>
    </dependency>
    <dependency>
        <groupId>com.google.code.findbugs</groupId>
        <artifactId>annotations</artifactId>
        <version>${findbugs.annotations.version}</version>
    </dependency>
    <dependency>
        <groupId>com.google.code.findbugs</groupId>
        <artifactId>jsr305</artifactId>
        <version>${findbugs.version}</version>
    </dependency>
</dependencies>


Tests for Generated Code

To test the generated code, I will create a testing controller that implements the generated interface. It will be quite simple.

TestController.class

Java
 
package org.example;

import org.example.server.api.ReportApi;
import org.example.server.api.model.GetReportAccountRequestHttp;
import org.example.server.api.model.GetReportAccountResponseHttp;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

@RestController
class TestController implements ReportApi {

    @Override
    public ResponseEntity<GetReportAccountResponseHttp> generate(final @Valid @RequestBody GetReportAccountRequestHttp request) {
        final var response = new GetReportAccountResponseHttp();
        response.setFile(Base64.getEncoder().encodeToString("test report".getBytes(StandardCharsets.UTF_8)));
        return ResponseEntity.ok(
                response
        );
    }
}


I create a response entity with some data to simulate normal controller behavior. 

I decided to use spring-test libraries, but not the spring boot test. I think it’s useful to configure required beans that help to understand how validation works in Spring. So, let’s configure Spring to have validation work correctly. 

Spring has its own framework for validation, so we need an adaptor to make Javax (Jarakta) validation work with Spring. And Spring provided such an adaptor it is org.springframework.validation.beanvalidation.LocalValidatorFactoryBean.

jakarta.validation.Validator comes to org.springframework.validation.Validator through SpringValidatorAdapter.

ConstraintValidator comes to org.springframework.validation.Validator using both SpringConstraintValidatorFactory, which helps to add validators to IoC containers and HibernateValidator, which implements ValidationProvider, to create required ValidatorFactory.

Besides, Spring uses Spring AOP for validation. AOP creates proxies for beans and adds validation interceptors where it is required. When starting the application context, Spring searches for classes marked with org.springframework.validation.annotation.Validated and creates validators for arguments marked with javax.validation.Valid. This job is done by org.springframework.validation.beanvalidation.MethodValidationPostProcessor.

So, we need at least two beans in the IoC container to make validation work. 

Besides, as far as you remember, I used custom messages in all validation annotations. However, to be more precisely, I used some codes, like

YAML
 
x-constraints: "/*not null value*/@NotNull(message = \"{notnull.account.agreement.type}\")"


But, actually, I want to see something like Account number shouldn't be null or empty as an error message, not some code like notnull.account.agreement.type.

So, we need a mapping between the code used in validation annotation and the error message. This mapping is configured for LocalValidatorFactoryBean using org.springframework.context.MessageSource. 

If you decide to configure something else, you are welcome, I will stop at this point, and my test configuration for Javax (Jakarta) validation with Spring is

TestConfiguration.class

Java
 
package org.example;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

import java.nio.charset.StandardCharsets;

@Configuration
class TestConfiguration {

    @Bean
    MessageSource messageSource() {
        final var messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:ValidationMessages.properties");
        messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
        return messageSource;
    }

    @Bean
    LocalValidatorFactoryBean localValidatorFactoryBean(final MessageSource messages) {
        final var bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messages);
        return bean;
    }


    @Bean
    MethodValidationPostProcessor validationPostProcessor() {
        final var processor = new MethodValidationPostProcessor();
        processor.setProxyTargetClass(true);
        return processor;
    }
}


and ValidationMessages.properties located in src\test\resources

ValidationMessages.properties

Plain Text
 
# suppress inspection "UnusedProperty" for whole file
notnull.account.number = Account number shouldn't be null or empty 
notnull.account.agreement = Account agreement shouldn't be null or empty
notnull.account.date = Account date shouldn't be null or empty
notnull.account.agreement.type = Account agreement type shouldn't be null or empty
notnull.account.param-name = Parameter name shouldn't be null or empty
notnull.account.param.id = Id account param shouldn't be null or empty
notnull.account.param.status = Status account param shouldn't be null or empty
notnull.account.param.siebel.id = SiebelId account param shouldn't be null or empty


So, here is the test.

SimpleTest.class

Java
 
package org.example;

import org.assertj.core.api.Assertions;
import org.example.common.model.AccountParameter;
import org.example.common.model.Parameters;
import org.example.server.api.model.AccountReportDataHttp;
import org.example.server.api.model.GetReportAccountRequestHttp;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@ExtendWith({SpringExtension.class})
@ContextConfiguration(
        classes = {
                TestController.class, TestConfiguration.class
        }
)
class SimpleTest {
    @Autowired TestController controller;

    @ParameterizedTest
    @MethodSource("testData")
    void test(final GetReportAccountRequestHttp request, final List<String> violations) {
        Assertions.assertThatThrownBy(
                () -> controller.generate(request)
        )
                .isExactlyInstanceOf(ConstraintViolationException.class)
                .extracting(e -> ((ConstraintViolationException) e).getConstraintViolations())
                .extracting(v -> v.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList()))
                .usingRecursiveComparison()
                .ignoringCollectionOrder()
                .isEqualTo(violations);
    }

    static Stream<Arguments> testData() {
        return Stream.of(
                Arguments.arguments(
                        new GetReportAccountRequestHttp(),
                        List.of(
                                "Id account param shouldn't be null or empty",
                                "SiebelId account param shouldn't be null or empty",
                                "Status account param shouldn't be null or empty"
                        )
                ),
                Arguments.arguments(
                        new GetReportAccountRequestHttp().payload(new AccountReportDataHttp()),
                        List.of(
                                "Id account param shouldn't be null or empty",
                                "SiebelId account param shouldn't be null or empty",
                                "Status account param shouldn't be null or empty",
                                "Account agreement type shouldn't be null or empty",
                                "Account date shouldn't be null or empty",
                                "Account number shouldn't be null or empty ",
                                "Account agreement shouldn't be null or empty"
                        )
                ),
                Arguments.arguments(
                        new GetReportAccountRequestHttp()
                                .payload(
                                        new AccountReportDataHttp().addParametersItem(AccountParameter.builder().build())
                                ),
                        List.of(
                                "Parameter name shouldn't be null or empty",
                                "Id account param shouldn't be null or empty",
                                "SiebelId account param shouldn't be null or empty",
                                "Status account param shouldn't be null or empty",
                                "Account agreement type shouldn't be null or empty",
                                "Account date shouldn't be null or empty",
                                "Account number shouldn't be null or empty ",
                                "Account agreement shouldn't be null or empty"
                        )
                ),
                Arguments.arguments(
                        new GetReportAccountRequestHttp()
                                .payload(
                                        new AccountReportDataHttp()
                                                .addParametersItem(
                                                        AccountParameter.builder()
                                                                .name(Parameters.ID)
                                                                .value("2a188887-dfb9-4282-9b8f-388814208113")
                                                                .build()
                                                )
                                                .addParametersItem(
                                                        AccountParameter.builder()
                                                                .name(Parameters.STATUS)
                                                                .value("OPENED")
                                                                .build()
                                                )
                                                .addParametersItem(
                                                        AccountParameter.builder()
                                                                .name(Parameters.SIEBEL_ID)
                                                                .value("76868247")
                                                                .build()
                                                )
                                                .agreementNumber("123712367868")
                                                .accountNumber("407028101261786384768234")
                                                .date(LocalDate.now())
                                ),
                        List.of(
                                "Account agreement type shouldn't be null or empty"
                        )
                )
        );
    }
}


In every test, we get ConstraintViolationException exception, because GetReportAccountRequestHttp is not completely filled.  ConstraintViolationException contains a set of violations, and we can verify these are exactly those violations we configured in api.yaml and common.yaml. So, everything works fine, which means we generated the code correctly.

Before finishing, I think it’s reasonable to provide test dependencies as well.

Java
 
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${spring-version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${spring-version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.20</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.10.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.10.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.10.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>javax.el-api</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>3.0.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.24.2</version>
    <scope>test</scope>
</dependency>


Conclusion

Finally, we generated the HTTP server code using the open API generator. We found out that it generates classes using mustache templates, and we can extend or amend its functionality by changing these templates. Besides, we can use vendor extensions. From version to version, new features are added, and some features are deprecated; however, the basic concept remains the same, at least up to the current version, so you can take all approaches discussed above. 

If you want to try examples from this article by yourself use the same versions, in this case everything will work fine without any efforts.

Enjoy your OpenAPI generator!

OpenAPI Specification Software development kit Mustache (template system) Template

Opinions expressed by DZone contributors are their own.

Related

  • Automated Bug Fixing: From Templates to AI Agents
  • Publishing Flutter Packages to JFrog Artifactory
  • Dynamic File Upload Component in Salesforce LWC
  • Simplifying Multi-Cloud Observability With Open Source

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!