OpenAPI specification to Spring REST microservice
The goal of the article is to show how to use OpenAPI Generator, which is a code generation tool, to create API client libraries, server stubs, documentation and configuration given an OpenAPI specification. There are already many languages and frameworks supported by the community, but the focus will be on how we can extend the existing Java generator in order to create a Spring REST microservice capable of fully handling operations defined in an OpenAPI specification. This will come in handy to quickly generate services such as Master Data which are simply designed to handle CRUD (create, read, update and delete) operations on each of the entities they manage, and to ensure an API-First approach. From a more generic point of view the aim is to generate services sharing the same structure, without the need of copy-pasting any base configuration from one to another, which will result in a great benefit in terms of time and cost in the long run.
OpenAPI Generator setup
The first step is to install the OpenAPI generator CLI. Once that is done, everything is set to start using all available generators provided by the tool. You can retrieve a list of available generators by running the command:
They are categorized as follows :
- CLIENT generators such as Ada, Android and Apex
- SERVER generators such as Ada Web Server, ASP.NET Core and C++ Pistache Server
- DOCUMENTATION generators such as AsciiDoc and DHTML
- SCHEMA generators such as Avro schema, GraphQL schema and Ktorm schema
- CONFIG generators such as apache2 the Apache Web Server config file
These templates are meant to be generic and are useful if the need is just to have a basic project structure that reflects the OpenAPI specification and that can be extended later as per requirements. Unfortunately when the need is to create multiple projects sharing the same pattern (e.g. same database configuration, same security configuration, same README template, …) the use of default generators can become time consuming. The solution in this case is to use the built-in Meta generator to define a new template set and a new configuration for the code generator.
Meta generator
A new template set can be generated using the command:
The options we can provide are:
- language: The implementation language for the generator class. We’ll proceed with
java
- name: The name of the generator. We’ll proceed with
rest-codegen
- output directory: Where to write the generated files (current directory by default)
- package: The package to put the main class into (defaults to
org.openapitools.codegen
) - type: The type of generator that will be created. We’ll proceed with
SERVER
After command execution, the output folder will contain:
- A RestCodegenGenerator class: This class is where we’ll define all custom configuration logic that will be used during the generation phase. By default the class extends the
DefaultCodegen
class but it is worthwhile to make it extend theAbstractJavaCodegen
abstract class in order to inherit a great part of the already defined logic for the Java language. The final hierarchy will look like this:
- Some default mustache templates: The project will be provided together with some default templates for OpenAPI paths (
api.mustache
) and data models (model.mustache
) in the directorysrc/main/resources/rest-codegen
. These are intended to be examples and cannot be used as is:
- An org.openapitools.codegen.CodegenConfig file: This file represents a service provider configuration file and will be used by the service loader to load the
RestCodegenGenerator
class during the generation phase - A default integration test class RestCodegenGeneratorTest: This test allows to easily launch the
CrudCodegenGenerator
class under a debugger to facilitate the development phase. It does not perform any assertions
Customizations
There are different files we need to modify to make the sample project, generated using the meta
command, match our requirements:
- RestCodegenGenerator class
- Mustache templates
RestCodegenGenerator
As mentioned before this class will contain all the custom configuration logic. There are plenty of methods that are declared in the base interface CodegenConfig
and that we can override to apply our logic. Some of them have already been overridden by the abstract class AbstractJavaCodegen
and can be reused as is, so we’ll look at the most important ones:
- processOpts
- preprocessOpenAPI
- postProcessOperationsWithModels
- postProcessAllModels
- addMustacheLambdas
- setTemplatingEngine
Before proceeding with the methods, we’ll look at the available template categories in order to understand how to organize our mustache templates to obtain the desired scaffolding in the target project.
Template categories
When we register all our mustache templates, we must be aware of their individual categories and effects on the generated file:
- Supporting files: These are files with 1:1 mapping, meaning that each template will be used for the generation of only one output file. Good candidates for this category are:
pom.xml, application.yml, Spring configuration classes and .gitlab-ci.yml
. For each output file we’ll be able to decide the path. - Model template files: These are files with 1:N mapping, which means each template will be used for the generation of N classes, depending on how many models are defined in the OpenAPI specification. Good candidates for this category are all those templates that represent the POJOs (Plain Old Java Object) structure for the target project. By default all output files will be written to the same configurable location that is decided by the
modelPackage
property. - API template files: Like the models, these files also have an 1:N mapping with N depending on how many different paths are defined in the OpenAPI specification. Good candidates for this category are all those templates that represent the
Controllers
structure for the target project. By default all output files will be written to the same configurable location that is decided by theapiPackage
property. - API Doc template files: All the rules defined above for APIs are applied to this category. As the name suggests, this category is meant for documentation templates related to APIs, but considering the context we are working in and the features of this category, some good candidates can be all those templates which represent the
Services
orService tests
structure for the target project. - API Test template files: All the rules defined for APIs are also applied to this category. Good candidates for this category are all those templates which represent the
Controller tests
structure for the target project.
Given the above, we’ll look now at the main overridable methods. To have a clear overview of the generation phase and to understand the exact point where the following methods are used, we can have a look at the DefaultGenerator
class.
void processOpts()
This is one of the first methods called before starting the actual generation phase, specifically when configuring generator properties. Actions done here will be visible in the next steps so, for example, it is ideal to configure:
- Global template properties
- Template source directory
- API package
- Model package
- Template categories
void preprocessOpenAPI(OpenAPI var1);
This method is called right before finalizing the OpenAPI structure read from the input specification, so it is the best place to apply any customization we need, like replacing response header references ($ref:'/components/headers/trace-id'
) with an actual definition that we’ll be able to use inside mustache templates:
A sample OpenAPI object, which is the Java representation of the input spec and the input of this method, can be found here.
Map<String, Object> postProcessOperationsWithModels(Map<String, Object> operations, List<Object> models);
This method is called after model template files have been generated and prior to generating API-related files. It also has the responsibility of returning the final operations structure that will be considered for API generation later. It’s the right place to apply any updates, like adding some additional properties to use inside mustache templates. For example we can mark them as CRUD if some requirements are matched and use that information in the templates to make some decisions.
A sample operations input can be found here and a models one here.
Map<String, Object> postProcessAllModels(Map<String, Object> models);
This method is called right before starting the generation of models. It also has the responsibility of returning the final models structure, so this is the right place to apply any update to them. For example we can add additional models for enums that, based on how they are declared in the OpenAPI specification, may be inserted inside the parent POJOs instead of creating new classes specific to them.
A sample models input can be found here.
protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas();
This method is quite important as it permits us to register some custom lambdas
. These represent a callable object, that receives a block of text and whose outcome will be rendered during template processing. They are really useful to perform complex actions that would not be possible to achieve using just Mustache templates tags. For example if we want to convert a string to lowercase inside the templates we can make use of the built-in LowercaseLambda: {{#lambda.lowercase}}FRAGMENT TO LOWERCASE{{/lambda.lowercase}}
. The following is a sample implementation of this method where a JavaDocDateLambda is added to the built-in ones:
void setTemplatingEngine(TemplatingEngineAdapter var1);
This method lets us decide which template engine to use when generating. The available options are:
- MustacheEngineAdapter: Active by default, uses the Mustache compiler
- HandlebarsEngineAdapter: Uses the Handlebars compiler
Mustache templates
As soon as the configuration is done the next step is about building mustache templates capable of generating all the files necessary for our Spring REST microservice. Mustache is a logic-less
template engine which means there are no conditional or iterative statements. The only way to handle this kind of complex scenario is to manipulate data injected into templates with the use of lambdas
and collections
.
As mentioned above templates are divided into 3 main categories:
Supporting file templates
This category represents all those templates that are added with the property supportingFiles
. Inside these templates, we can only access data added to their context.
Example of a supporting file template fragment:
Model templates
This category represents all those templates that are added with the property modelTemplateFiles
. Inside these templates, we can only access data added to their context.
Example of a model template fragment:
API templates
This category represents all those templates that are added with the properties apiTemplateFiles, apiDocTemplateFiles, apiTestTemplateFiles
. Inside these templates, we can only access data added to their context.
Example of an API template fragment:
Generation
Once both templates and generator configuration are ready, everything is set to start with the target project generation. In order to do that, the following command can be used:
It requires:
- The path to our
meta
project jar file - The path to the
openapi-generator-cli
jar file - The generator name we chose when executing the
meta
command - The path to the OpenAPI specification file
- The path of the target project
- The path to the
config.json
file
As we can see with the previous command, we are required to have the openapi-generator-cli
jar file on our filesystem. This overhead can be avoided by adding the openapi-generator-cli
dependency to our project and by using the maven-shade-plugin
to produce the complete artifact with dependencies included:
By adding the above to the pom.xml
file inside our meta
project, the generation command can be rewritten as:
config.json
The config.json
file contains all those properties that are generator specific and where we want to set custom values. Since we are using Java in our sample project as the base language, we can look at the related config options by running the following command:
These properties will be added automatically in the context for all templates:
Debugging
During the development phase, debugging gives us a huge help to understand the generation flow and to look at what contexts are given to each template category. Also since the code readability is a little bit reduced by the use of java.lang.Object classes, debugging becomes a must. In order to achieve that we’ll have to add the following argument to the JVM when using the generation command:
By doing this we instruct the JVM to wait until a remote debugger connects to the configured port. After that the execution can proceed. On the other side, in the IDE, we’ll have to configure the remote debugger in the following way:
Caveats
One noticeable con of this generation approach is the amount of time initially required to create the templates. The more complex the logic we want to implement, the more effort we’ll have to initially put in and will be required on each update as the risk of breaking a previous rule will be high. In order to reduce this risk and to try to limit the initial time investment, some basic rules can be followed:
- Find a trade-off between the initial effort and the time it takes to adapt the target project manually. In other words, if an update on the meta project is complex and potentially required for a limited subset of target projects, consider adapting the target project manually instead.
- Avoid including too many rules in a single meta project. If we have multiple scenarios to handle, maybe it’s worthwhile to create a dedicated meta project for each of them rather than trying to fit them all into the same one.
Conclusion
This generation approach fits our requirements, even considering the amount of time initially required to create the templates. The initial investment will be completely repaid as each new project will already contain the important files that we used to copy-paste from other projects. In the long run, this will save us a lot of time.
Work with us
If you are looking for new job opportunities, check out what we’re offering at TUI Musement:
https://medium.com/tuitech/join-our-team-in-tui-musement-ad2e51cfd722