Enterprise Java

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 with super(). The outputFolder 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 with User.
  • {{#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 as id, name, or email in a User class.
  • {{datatype}}:
    • This placeholder within the loop represents the data type of each variable, such as String, int, or boolean, based on the OpenAPI specification’s type definitions.
    • For instance, if name is defined as a string type, {{datatype}} will be replaced with String.
  • {{name}}:
    • This placeholder represents the actual name of the variable within the class.
    • For example, in the User model, the name attribute will be used to generate private 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 of MyCustomGenerator and verifies that the generator’s name is set correctly. The method calls generator.getName() and uses assertEquals to confirm that it returns "my-custom-generator", as expected.
  • The testOutputFolder method also creates an instance of MyCustomGenerator and checks if the outputFolder variable contains the expected value, "my-custom-generator". It uses assertTrue to validate that the outputFolder 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.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button