Generating API clients based on OpenAPI v3 specifications

Java Microservices REST OpenAPI Spring
Dominik Jastrzębski photo
Dominik Jastrzębski
06 Nov 2019
5 min read

Microservice architecture is one of the recent buzzwords in the world of software development. It can help create scalable, robust software even in complex business domains. However, it introduces additional operational complexity among which is communication between many services.

Regarding service-to-service communication, there are several questions to answer:

  • which protocol(s) should we use?
  • how do we manage API changes?
  • how do we document our APIs?

Using REST APIs is a very common choice for synchronous communication. The OpenAPI standard has been introduced to standardize the way developers create API documentation and provide a user-friendly UI to browse the APIs.

In environments including multiple services communicating with each other a lot of effort has to be put in creating API clients, passing request parameters, creating DTOs, etc. What if the client code could be generated automatically based on API documentation that needs to be created anyway? Let’s see!

Setup

We will be using OpenAPI Generator to generate Java clients for our microservices based on OpenAPI YAML files containing the documentation. There is also support for other programming languages.
The sample Java web application is available on GitHub: https://github.com/98elements/openapi3-codegen-demo.

Sample Application

The sample application is created using Java and Spring Boot. Groovy and Spock are used for testing.

The API that we want to consume is http://petstore.swagger.io:8080. It contains CRUD operations for a pet store. We will focus on POST /pet and GET /pet/{petId}.

Let’s create a domain object representing a pet:

public class Pet {

   private final Long id;

   private final String name;

   private final PetStatus status;

   private final List<String> photoUrls;
   
   ...
}

and a client interface to manage objects representing pets:

public interface PetClient {

   void create(final Pet pet);

   Pet get(final long petId);

}

then we can start the implementation:

@Component
class PetClientImpl implements PetClient {

   @Override
   public void create(final Pet pet) {
       ...
   }

   @Override
   public Pet get(final long petId) {
       ...
   }
}

If we were to create the client manually, we would have to:

  • define request/response objects and DTOs
  • handle how parameters are passed (query string, body, headers)
  • interact with the HTTP client
  • write the request and read the response
  • handle status codes

Although there are solutions saving us some work (e.g. Feign), we are still duplicating information that’s already available in the service’s documentation.

Let’s use the Petstore service documentation (available in YAML format at http://petstore.swagger.io:8080/api/v3/openapi.yaml) to generate an API client.

We need to include the OpenAPI Generator Gradle plugin to build.gradle:

plugins {
   ...
   id 'org.openapi.generator' version '4.1.3'
}

and the required dependencies:

dependencies {
   ...
   compile 'io.swagger:swagger-annotations:1.5.24'
   // For the @Nullable annotation.
   compile 'com.google.code.findbugs:jsr305:3.0.2'
   ...
}

Then we declare a Gradle task that will create the client during compile time:

openApiGenerate {
   generatorName = "java"
   library = "resttemplate"
   inputSpec = "$rootDir/specs/petstore.yaml".toString()
   outputDir = "$buildDir/generated".toString()
   apiPackage = "com._98elements.openapi3codegendemo.infrastructure.openapi.pets"
   invokerPackage = "com._98elements.openapi3codegendemo.infrastructure.openapi.pets"
   modelPackage = "com._98elements.openapi3codegendemo.infrastructure.openapi.pets.dto"
   modelNameSuffix = "Dto"
   configOptions = [
           dateLibrary: "java8"
   ]
}

OpenAPI generator supports many programming languages and HTTP libraries. Let’s stick with Java and RestTemplate.

Then we specify the location of the YAML file (inputSpec) containing the service’s documentation and the outputDir where the generated classes will be located.

We are also able to configure the packages where the generated client’s classes will be placed in. It’s useful to specify a Dto prefix for the generated classes so they won’t be confused with our own domain classes (without using fully qualified names).

Let’s add this line of configuration to tell Gradle that it should generate the client before compiling the project:

compileJava.dependsOn tasks.openApiGenerate

Now we are ready to generate the client!

./gradlew openApiGenerate

The generated API client is named PetApi and already contains Spring configuration:

@Component("com._98elements.openapi3codegendemo.infrastructure.openapi.pets.PetApi")
public class PetApi {
...

So we can just inject it in PetClientImpl and implement our client:

@Component
class PetClientImpl implements PetClient {

   private final PetApi petApi;

   PetClientImpl(final PetApi petApi) {
       this.petApi = petApi;
   }

   @Override
   public void create(final Pet pet) {
       petApi.addPet(
               new PetDto()
                       .id(pet.getId())
                       .name(pet.getName())
                       .status(PetDto.StatusEnum.valueOf(pet.getStatus().name()))
                       .photoUrls(pet.getPhotoUrls())
       );
   }

   @Override
   public Pet get(final long petId) {
       final PetDto petDto = petApi.getPetById(petId);
       return new Pet(
               petDto.getId(),
               petDto.getName(),
               PetStatus.valueOf(petDto.getStatus().name()),
               petDto.getPhotoUrls()
       );
   }
}

The only thing we need to take care of is:

  • mapping between DTOs and domain objects (which we could get rid of with help of ModelMapper or MapStruct)
  • handling RestClientExceptions if needed

Let’s test the client against http://petstore.swagger.io:8080 (the publicly available implementation of the service) and check if it works!

Testing

At first, we need to configure basePath of the server that we use:

@Configuration
class PetApiConfiguration {

   @Autowired
   private ApiClient apiClient;

   @PostConstruct
   void setupPetApiUrl() {
       apiClient.setBasePath("http://petstore.swagger.io:8080/api/v3");
   }
}

Let’s create a simple test storing pet data in the service and retrieving it afterward:

@SpringBootTest
class PetClientImplntegrationTest extends Specification {

   @Autowired
   PetClient petClient

   def 'should create a pet'() {
       given:
       def photoUrl = 'https://cdn.pixabay.com/photo/2017/02/20/18/03/cat-2083492_1280.jpg'
       def pet = new Pet(1, 'cat', PetStatus.AVAILABLE, [photoUrl])

       when:
       petClient.create(pet)

       then:
       petClient.get(pet.id) == pet
   }
}
./gradlew test
BUILD SUCCESSFUL in 8s

We successfully generated a REST API client from an OpenAPI doc!

New OpenAPI v3 Features

One of the features introduced in OpenAPI v3 for generated Java clients is sharing enums between API models. Let’s see how it can help reduce boilerplate in OpenAPI files and simplify handling generated DTO objects.

Let’s take a look at the status enum from petstore.yaml:

Pet:
  ...
    status:
      type: string
      description: pet status in the store
      enum:
      - available
      - pending
      - sold

The enumerated values are inlined inside the containing model - Pet. If we want to include this enumeration in another model, we have two ways to do so.

The first way is to duplicate the definition in the other model:

PetListItem:
  ...
    status:
      type: string
      description: pet status in the store
      enum:
      - available
      - pending
      - sold

Let’s see how the generated code looks like in this case:

public class PetDto {
  ... 
  public enum StatusEnum {
   AVAILABLE("available"),
   PENDING("pending"),
   SOLD("sold");
   ...
  }
}

public class PetListItemDto {
  ... 
  public enum StatusEnum {
   AVAILABLE("available"),
   PENDING("pending"),
   SOLD("sold");
   ...
  }
}

As a result, the same enumeration is defined twice in the generated code. Plus, we have duplication in the YAML file. Until OpenAPI v3, we had no other options.

But in OpenAPI v3, we can define an enum that be shared between models:

PetStatus:
  type: string
    description: pet status in the store
    enum:
    - available
    - pending
    - sold
Pet:
  ...
    status:
      $ref: '#/components/schemas/PetStatus'
PetListItem:
  ...
    status:
      $ref: '#/components/schemas/PetStatus'

With this approach, PetStatus becomes a standalone enum that can be referenced in multiple models:

public enum PetStatusDto {
 AVAILABLE("available"),
 PENDING("pending"),
 SOLD("sold");
 ...
}

public class PetDto {
  private PetStatusDto status;
  ...
}

public class PetListItemDto {
  private PetStatusDto status;
  ...
}

Read More

You can read more about OpenAPI Generator here: https://github.com/OpenAPITools/openapi-generator and about the OpenAPI standard here: https://github.com/OAI/OpenAPI-Specification.

Build your backend with us

Your team of exceptional software engineers