OpenAPI Custom Generator
OpenAPI is a powerful tool for generating code from API specifications, but sometimes, default generators do not meet specific project needs. Let us delve into understanding the creation of a Java OpenAPI custom generator.
1. Introduction
The OpenAPI Specification is a standardized format widely used for defining and documenting RESTful APIs, ensuring consistency and ease of use across different platforms and languages. To facilitate the development process, the OpenAPI Generator is a tool that enables developers to generate various assets directly from an API specification, such as client SDKs, server stubs, and documentation. By taking an OpenAPI Specification document as input, the OpenAPI Generator can automatically create these assets, significantly reducing development time and minimizing manual coding errors.
The OpenAPI Generator comes with a wide range of built-in generators that support multiple programming languages and frameworks. These built-in generators allow developers to quickly generate client libraries and server implementations in languages such as Java, Python, JavaScript, and more, helping them integrate APIs efficiently into their applications. Additionally, the OpenAPI Generator can produce comprehensive documentation in formats like HTML and Markdown, making it easier to share and understand API details.
Despite the versatility of these built-in generators, certain projects may have unique requirements that are not fully addressed by the default configurations. In such cases, developers can create a custom generator. A custom generator allows developers to define specific templates and settings that tailor the generated output to the particular needs of their project, such as adding custom file structures, and templates, or even modifying the default behavior of the generator. By extending or modifying the OpenAPI Generator, developers can produce assets that align more closely with their project’s standards, ensuring compatibility and enhancing maintainability.
1.1 Why Create a New Generator?
Creating a new OpenAPI Generator provides significant advantages, particularly when standard generators don’t fully meet the specific needs of a project. Custom generators allow developers to go beyond what is available out of the box and create API assets that are more closely aligned with project requirements.
- Customization: A custom OpenAPI generator offers the ability to produce code that aligns with specific coding standards, organizational preferences, and the overall structure of the project. By creating a tailored generator, developers can incorporate naming conventions, file structures, and annotations that fit seamlessly into their existing codebase. This customization can improve maintainability and make the generated code more compatible with other parts of the project.
- Additional Features: Custom generators allow for adding unique features or documentation that the default OpenAPI generators may not support. For instance, developers require additional data validations, specific exception-handling mechanisms, or support for custom authentication methods within the generated code. A custom generator makes it possible to include such enhancements, delivering assets that fully reflect the API’s functionality and design. Developers can also customize generated documentation to include more project-specific instructions, usage examples, or visual aids, improving clarity and usability for end-users.
- Enhanced Output: A custom generator can modify the output format to improve readability or ensure compatibility with additional tools and services. This includes altering indentation, line breaks, and general layout, which can make the code easier to review and integrate into the development workflow. Additionally, custom formatting options can make the generated assets compatible with tools for version control, deployment pipelines, or specific IDEs, thereby streamlining collaboration and deployment processes.
Overall, building a custom OpenAPI generator allows teams to create optimized, standards-compliant, and feature-rich assets that integrate seamlessly with their project’s ecosystem, making it a valuable approach for more complex or specialized API requirements.
1.2 Benefits of OpenAPI Generator
- Increased Development Efficiency: Automates the creation of API client SDKs, server stubs, and documentation, reducing repetitive coding tasks.
- Consistency Across Codebases: Ensures that all API clients, server stubs, and documentation align with the latest API specification, improving consistency across projects.
- Language and Framework Flexibility: Supports multiple languages and frameworks, making it versatile for diverse development environments.
- Rapid Prototyping: Enables quick API prototyping by generating server and client code directly from specifications, helping developers test and iterate faster.
- Enhanced Documentation: Generates detailed API documentation based on the OpenAPI spec, improving API transparency and accessibility for users.
- Standardization of APIs: Facilitates API standardization across teams by using a unified specification and generation tool, ensuring consistency and maintainability.
- Customizability: Allows custom generators and templates to meet unique project needs, making them adaptable to different coding standards and architectural styles.
1.3 Use Cases for OpenAPI Generator
- Client SDK Generation: Automatically create client libraries in multiple languages (e.g., Java, Python, JavaScript) to enable faster and more consistent API consumption.
- Server Stub Generation: Generate server stubs to quickly set up backend endpoints and serve as a foundation for implementing business logic.
- Microservices Development: Facilitate microservices communication by generating API clients and ensuring all services interact consistently.
- API Documentation: Automatically generate interactive API documentation, such as HTML docs or Markdown files, to improve API usability.
- Testing API Endpoints: Use generated clients and mocks for API testing to ensure endpoints function as expected before release.
- API Versioning and Maintenance: Update and regenerate code for different API versions to handle changes with minimal manual effort.
- Rapid Prototyping for MVPs: Quickly prototype and test Minimum Viable Products (MVPs) by generating code from the API spec for faster iterations.
2. Creating an OpenAPI Generator Project
Setting up a new generator project involves creating a Maven or Gradle project and configuring it to work with the OpenAPI Generator’s codebase. Below is a step-by-step guide:
2.1 Add Dependencies
Add the necessary dependencies to the pom.xml
file:
<dependencies> <dependency> <groupId>org.openapitools</groupId> <artifactId>openapi-generator</artifactId> <version>your_jar_version</version> </dependency> </dependencies>
2.2 Implementing the Generator
2.2.1 Creating a Generator class
OpenAPI generators extend from the AbstractJavaCodegen
class or other base classes provided by the OpenAPI Generator library. Let’s understand the generator class below.
package com.example.generator; import org.openapitools.codegen.languages.AbstractJavaCodegen; import org.openapitools.codegen.CodegenConfig; public class MyCustomGenerator extends AbstractJavaCodegen implements CodegenConfig { public MyCustomGenerator() { super(); outputFolder = "generated-code/my-custom-generator"; modelTemplateFiles.put("model.mustache", ".java"); } @Override public String getName() { return "my-custom-generator"; } @Override public void processOpts() { super.processOpts(); apiTemplateFiles.put("api.mustache", ".java"); supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); } }
2.2.1.1 Code explanation
The code defines a custom generator in Java, named MyCustomGenerator
, which extends the AbstractJavaCodegen
class and implements the CodegenConfig
interface from the OpenAPI tools library.
- In the constructor
MyCustomGenerator()
, the superclass’s constructor is called withsuper()
. TheoutputFolder
is set to"generated-code/my-custom-generator"
, which specifies the directory where the generated files will be stored. - A model template is added by mapping
"model.mustache"
files to generate.java
files. - The
getName()
method returns the name of the generator,"my-custom-generator"
. This is used by OpenAPI tools to recognize the generator. - The
processOpts()
method, which handles additional processing options, is overridden. It first calls the superclass’s method, then maps"api.mustache"
templates to.java
files and adds a supporting file,README.md
, generated from the"README.mustache"
template.
This generator customizes the code generation process for OpenAPI specifications.
2.2.2 Creating a Mustache template
Mustache templates define how the generated code will be structured. A basic model.mustache
template look like this:
package {{package}}; public class {{classname}} { {{#vars}} private {{datatype}} {{name}}; {{/vars}} }
Place this template in the templates folder within your project.
2.2.2.1 Code explanation
The template provided is used to generate Java classes based on an OpenAPI specification. This template is used within the OpenAPI Generator to create Java model classes dynamically by substituting the placeholders with values from the OpenAPI specification.
- {{package}}:
- This placeholder is replaced by the package name defined in the OpenAPI configuration or input.
- It ensures that each generated class is placed in the correct package namespace.
- {{classname}}:
- This placeholder is replaced by the name of the class generated for a specific model defined in the OpenAPI specification.
- For example, if the model is
User
,{{classname}}
will be replaced withUser
.
- {{#vars}} and {{/vars}}:
{{#vars}}
starts a loop over a list of properties (or variables) within the class, and{{/vars}}
ends this loop.- Each
vars
item represents an attribute (e.g., a field of a model) defined in the OpenAPI spec, such asid
,name
, oremail
in aUser
class.
- {{datatype}}:
- This placeholder within the loop represents the data type of each variable, such as
String
,int
, orboolean
, based on the OpenAPI specification’s type definitions. - For instance, if
name
is defined as a string type,{{datatype}}
will be replaced withString
.
- This placeholder within the loop represents the data type of each variable, such as
- {{name}}:
- This placeholder represents the actual name of the variable within the class.
- For example, in the
User
model, thename
attribute will be used to generateprivate String name;
.
3. Unit Testing
Unit testing is crucial for validating the logic within your generator. Using JUnit, we can verify the correctness of the generator’s behavior.
package com.example.generator; import org.junit.jupiter.api.Test; import org.openapitools.codegen.SupportingFile; import static org.junit.jupiter.api.Assertions.*; public class MyCustomGeneratorTest { @Test public void testGeneratorName() { MyCustomGenerator generator = new MyCustomGenerator(); assertEquals("my-custom-generator", generator.getName()); } @Test public void testOutputFolder() { MyCustomGenerator generator = new MyCustomGenerator(); assertTrue(generator.outputFolder.contains("my-custom-generator")); } @Test public void testProcessOpts() { MyCustomGenerator generator = new MyCustomGenerator(); generator.processOpts(); // Check that the model template file is set correctly assertTrue(generator.modelTemplateFiles.containsKey("model.mustache")); assertEquals(".java", generator.modelTemplateFiles.get("model.mustache")); // Check that the api template file is set correctly assertTrue(generator.apiTemplateFiles.containsKey("api.mustache")); assertEquals(".java", generator.apiTemplateFiles.get("api.mustache")); // Check that supporting files contain README.md assertTrue(generator.supportingFiles.stream().anyMatch( file -> file.destinationFilename.equals("README.md") )); } }
The code defines a test class named MyCustomGeneratorTest
, which tests the behavior of the MyCustomGenerator
class. It is part of the com.example.generator
package and uses the JUnit 5 testing framework, imported with org.junit.jupiter.api.Test
and org.junit.jupiter.api.Assertions.*
, to validate specific aspects of the MyCustomGenerator
implementation.
- The
testGeneratorName
method creates an instance ofMyCustomGenerator
and verifies that the generator’s name is set correctly. The method callsgenerator.getName()
and usesassertEquals
to confirm that it returns"my-custom-generator"
, as expected. - The
testOutputFolder
method also creates an instance ofMyCustomGenerator
and checks if theoutputFolder
variable contains the expected value,"my-custom-generator"
. It usesassertTrue
to validate that theoutputFolder
path includes this string, confirming that the output folder is configured correctly. testProcessOpts
: This new test case verifies the configurations that processOpts sets up:- It confirms that the modelTemplateFiles and apiTemplateFiles contain the right entries (“model.mustache” and “api.mustache” mapped to .java).
- It also checks that supportingFiles includes the README file.
Together, these test methods ensure that the MyCustomGenerator
class has the correct name and output folder settings, providing basic verification for key configurations of the generator. Running these tests will result in an output indicating whether each assertion passes or fails.
[INFO] Running MyCustomGeneratorTest [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.123 s [INFO] All tests passed.
4. Integration Test
Integration tests ensure that your custom generator works seamlessly with OpenAPI specifications. Below is a test setup that uses an example OpenAPI specification to generate code.
package com.example.generator; import org.junit.jupiter.api.Test; import org.openapitools.codegen.ClientOptInput; import org.openapitools.codegen.DefaultGenerator; import org.openapitools.codegen.config.CodegenConfigurator; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; public class MyCustomGeneratorIntegrationTest { @Test public void testGenerateCodeFromSpec() { // "example.yaml" is an OpenAPI specification file located in src/test/resources. // It defines the API endpoints, data models, and responses according to the OpenAPI standard. // This file provides the blueprint for generating the API client/server code with the custom generator. CodegenConfigurator configurator = new CodegenConfigurator() .setGeneratorName("my-custom-generator") .setInputSpec("src/test/resources/example.yaml") .setOutputDir("out/my-custom-generator"); // Converts configuration into the input format for code generation ClientOptInput input = configurator.toClientOptInput(); DefaultGenerator generator = new DefaultGenerator(); // Runs the generator with the provided specification, generating code in the output directory generator.opts(input).generate(); // Verification: Check if output directory contains generated files File outputDir = new File("out/my-custom-generator"); assertTrue(outputDir.exists() && outputDir.isDirectory(), "Output directory should exist and contain generated files."); } }
Run the test and verify that the generated output in out/my-custom-generator
matches your custom template’s specifications. Make note that the example.yaml
file will depend on your business requirements.
Below is the example.yaml
file used for this article:
openapi: 3.0.0 info: title: Sample API version: 1.0.0 description: A simple API for demonstrating OpenAPI code generation with custom settings. paths: /items: get: summary: Retrieve a list of items operationId: getItems responses: '200': description: A list of items. content: application/json: schema: type: array items: $ref: '#/components/schemas/Item' post: summary: Create a new item operationId: createItem requestBody: description: Details of the item to create required: true content: application/json: schema: $ref: '#/components/schemas/Item' responses: '201': description: Item created successfully. '400': description: Invalid input. /items/{itemId}: get: summary: Get details of a specific item operationId: getItemById parameters: - name: itemId in: path required: true schema: type: integer example: 1 responses: '200': description: Details of the specified item. content: application/json: schema: $ref: '#/components/schemas/Item' '404': description: Item not found. components: schemas: Item: type: object required: - id - name properties: id: type: integer example: 1 name: type: string example: "Sample Item" description: type: string example: "This is a sample item." price: type: number format: float example: 19.99
Since this test checks that the output directory is created, the result will indicate whether code generation was successful. If the output directory exists and contains generated files, the assertion will pass; otherwise, it will fail.
5. Using the Custom Generator
Run the custom generator from the command line with:
java -jar openapi-generator-cli.jar generate \ -g my-custom-generator \ -i /path/to/openapi.yaml \ -o /path/to/output
This command applies your custom generator to the specified OpenAPI specification file.
6. Conclusion
Creating a custom OpenAPI generator allows you to fully control the structure and design of the generated code. With the ability to customize templates, add new configurations, and create tests, you can integrate the generator seamlessly into your development workflow. This approach helps meet specific project requirements and ensures that the generated code adheres to your team’s coding standards.