Skip to content

Generating HTTP clients in Spring Boot application from OpenAPI spec

Published on
  • Spring Boot
  • Java
  • Openapi

In this step-by-step tutorial you will learn how to generate HTTP client code for Spring Boot application from OpenAPI spec using openapi-generator Gradle plugin.


OpenAPI has become a de-facto standard for defining & documenting HTTP based RESTful APIs. While Spring Boot does not come with OpenAPI support of the box, there are few 3rd party projects that fill the gap:

  • ePages-de/restdocs-api-spec - an extension to Spring REST Docs that adds OpenAPI spec generation capability
  • Springdoc - generates OpenAPI spec in runtime based on Spring MVC/Webflux controllers and project configuration
  • Springwolf - similar to Springdoc but for messaging/asynchronous APIs - generates AsyncAPI compliant spec for Kafka/RabbitMQ/JMS/SQS etc. integrations

All 3 projects mentioned above cover the server side of an API.

Since OpenAPI is a well-defined spec, it can be used also on the client side to generate client code. For this use case, there is one project that seems to be a community standard: https://openapi-generator.tech/.

OpenAPI Generator generates clients for various programming languages/frameworks. Depending on the chosen generator it may generate client or server code.

Today I am focusing only on generating http client code for Spring Boot application and document what I learned and how I adjusted generator to my needs, but you are welcome to explore its other functionalities.

Project Setup

The starting point is an empty Spring Boot project generated with start.spring.io with Spring Web dependency selected and Gradle build tool.

I am going to generate HTTP clients for Spring Petclinic Rest so I copied openapi.yml file to my project's root directory.

OpenAPI Generator installation

OpenAPI Generator comes as a CLI, Maven or Gradle plugin. Its functionality may differ in tiny details but in 99% it is the same. I'm going to use Gradle variant but the configuration should be easy to translate to Maven plugin.

First configure org.openapi.generator with the latest version in build.gradle:

groovy
plugins {
    // ...
	id 'org.openapi.generator' version '7.7.0'
}

Once plugin is configured, new Gradle tasks are available:

console
$ ./gradlew tasks
...
OpenAPI Tools tasks
-------------------
openApiGenerate - Generate code via Open API Tools Generator for Open API 2.0 or 3.x specification documents.
openApiGenerators - Lists generators available via Open API Generators.
openApiMeta - Generates a new generator to be consumed via Open API Generator.
openApiValidate - Validates an Open API 2.0 or 3.x specification document.
...

Configuration

Configure generator and library

Before the generator can be used, it must be configured. First we need to choose which generator to use. Complete list of available generators can be found at https://openapi-generator.tech/docs/generators - notice that this list has separate sections for client, server, documentation, spec and config. Since we are going to generate client code, only the entries under client section are interesting fo us.

At the moment of writing this article, there are 3 Java related generators available: java, java-helidon-client, java-micronaut-client. Does it mean there is no dedicated client for Spring? Not really.

Each generator has a long list of configuration options, and it happens that java generator can be used to generate client code variants compatible with a variety of different Java HTTP clients - from Feign, Retrofit, RestEasy, Vertx through native Java's HttpClient to Spring's RestTemplate, WebClient and RestClient.

INFO

At the moment of writing this article, there is no option to generate client code that uses Spring 6+ declarative interface based clients with @HttpExchange.

We are going to choose the "modern" approach and create clients based on RestClient:

In build.gradle add following section:

groovy
openApiGenerate {
	generatorName.set('java')
	configOptions.set([
		library: 'restclient'
	])
}

Find all possible configuration options for java generator in official documentation.

Configure spec location

OpenAPI generator can generate code from local or remote json or yaml file.

For local file, assuming the spec file is named petclinic-spec.yml and is located in the same directory as build.gradle file, set value for inputSpec property:

groovy
openApiGenerate {
	// ...
	inputSpec.set('petclinic-spec.yml')
}

For spec in remote location, instead of setting inputSpec, set remoteInputSpec:

groovy
openApiGenerate {
	// ...
	remoteInputSpec.set('http://some-service.net/v3/api-docs')
}

If I could choose JSON based spec or YAML - I would go with JSON. I had cases where YAML produced by other tools on the server side was either not a valid YAML or not valid enough for OpenAPI Generator to consume it. Did not have such problems with JSON.

Once spec and generators are set, you can call ./gradlew openApiGenerate to generate client code.

Generated code is located in build/generate-resources/main and looks far from what I expected:

While my intention is to generate just model classes mapping to requests/responses and a class representing the API, the generator generated complete Maven/Gradle project, including CI servers configuration, markdown documentation, Android Manifest and more. On one hand this may not bother you as this is not the code you are going to push to Git repository, but it still bothers me to have so many classes I simply don't need and won't use on the classpath. Let's try to trim it a bit and leave as little generated code as actually needed.

Remove unneeded code

OpenAPI generator supports ignore file similar to .gitignore where we can decide which files should be filtered out from project generation:

groovy
openApiGenerate {
    // ...
	ignoreFileOverride.set(".openapi-generator-java-sources.ignore")
}

Create file .openapi-generator-java-sources.ignore in project root directory with following content:

gitignore
*
**/*
!**/src/main/java/**/*

Now once you generate project again with ./gradlew openApiGenerate you'll see much less clutter:

There are still few classes you may want to filter out, but I'll leave it up to you. I am quite fine with such result.

Configure package name

As you have probably noticed, classes are generated in org.openapitools.client package. To change it configure few more properties:

groovy
openApiGenerate {
	// ...
	invokerPackage.set('com.myapp')
	modelPackage.set('com.myapp.model')
	apiPackage.set('com.myapp.api')
}

Include generated sources in project

Gradle does not know by default where the generated code is located, so to be able to use it in your main you must add it to Gradle SourceSet:

groovy
sourceSets.main.java.srcDir "${buildDir}/generate-resources/main/src/main/java"

Now when you run ./gradlew build it will compile both generated code and your own code. Surprisingly (or not), the compilation fails.

Exclude Jackson Nullable Module

The generated code relies on jackson-databind-nullable module. Since we removed all the generated pom.xml, build.gradle and just want to include generated Java classes, we either must add a dependency to this module, or configure OpenAPI plugin to not use it. I chose the latter:

groovy
openApiGenerate {
	configOptions.set([
	    // ...
		openApiNullable: 'false'
	])
}

Run openApiGenerate before compilation

Since I don't want to remember to re-run class generation each time it changes, I prefer to configure Gradle to re-generate classes each time I run build task or just compile the code:

groovy
tasks.named('compileJava') {
    dependsOn(tasks.openApiGenerate)
}

Complete build file

Complete build.gradle file with all the changes mentioned above for simpler copy & pasting:

groovy
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.2'
	id 'io.spring.dependency-management' version '1.1.6'
	id 'org.openapi.generator' version '7.7.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

tasks.named('compileJava') {
	dependsOn(tasks.openApiGenerate)
}

sourceSets.main.java.srcDir "${buildDir}/generate-resources/main/src/main/java"

openApiGenerate {
	generatorName.set('java')
	configOptions.set([
		library: 'restclient',
		openApiNullable: 'false'
	])
	inputSpec.set('petclinic-spec.yml')
	ignoreFileOverride.set(".openapi-generator-java-sources.ignore")
	invokerPackage.set('com.myapp')
	modelPackage.set('com.myapp.model')
	apiPackage.set('com.myapp.api')
}

Beans configuration

The generated code consists of 3 main parts:

  • ApiClient - the low level generic HTTP client that delegates to Spring's RestClient - it is not going to be used directly in our code
  • PetApi, OwnerApi etc. - high level client containing all operations per OpenAPI tag
  • model classes that map to JSON requests/responses

None of the classes is annotated with Spring @Component or @Service so beans must be created using Java configuration.

Since ApiClient delegates to RestClient, we may (and probably should) pass the auto-configured Spring's RestClient.Builder to ApiClient constructor to benefit from RestClient auto-configuration:

java
@Bean
ApiClient apiClient(RestClient.Builder builder) {
    var apiClient = new ApiClient(builder.build());
    apiClient.setBasePath("http://localhost:9966/petclinic/api");
    return apiClient;
}

@Bean
PetApi petApi(ApiClient apiClient) {
    return new PetApi(apiClient);
}

Set the base url

The base url can be set on both RestClient passed to ApiClient, and ApiClient itself through setBasePath method. It would be Spring-idiomatic to set it on the RestClient but unfortunately it does not work. ApiClient does not honour base url set on RestClient and the base path must be set on the ApiClient

Configure authentication

If API requires authentication, similar question arises as to where to configure the base url. The answer depends on if the spec file includes authentication requirements or not.

Configure auth on ApiClient

If API spec file contains authentication entry - for example basic auth:

yml
openapi: 3.0.1
info:
  title: Spring PetClinic
# ...
security:
  - BasicAuth: []
# ...
components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
# ...

Then authentication can be set on the ApiClient. ApiClient will apply authentication headers only on operations that require authentication:

java
@Bean
ApiClient apiClient(RestClient.Builder builder) {
    var apiClient = new ApiClient(builder.build());
    apiClient.setUsername("admin");
    apiClient.setPassword("admin");
    apiClient.setBasePath("http://localhost:9966/petclinic/api");
    return apiClient;
}

Configure auth on RestClient

If API's spec file does not contain authentication entry but API still requires authentication - authentication can be configured on the RestClient:

java
@Bean
ApiClient apiClient(RestClient.Builder builder) {
    var restClient = builder
            .requestInterceptor(new BasicAuthenticationInterceptor("admin", "admin"))
            .build();
    var apiClient = new ApiClient(restClient);
    apiClient.setBasePath("http://localhost:9966/petclinic/api");
    return apiClient;
}

In such case, authentication header will be passed to all requests executed through ApiClient.

Pros & Cons

The most obvious benefit of generating client code is:

  • ... that we don't have to write it ourselves - less effort to get things done
  • fewer chances of making silly mistake like requesting wrong URL or passing wrong payload.
  • easy to stay up to date and detect incompatible API changes

As usual, there are some drawbacks - generated code is far from the code I would write myself:

  • OpenAPI generates mutable classes for requests & responses - where they all should be immutable.
  • It does not utilize Java records (there are reasons and workarounds)
  • all XXXApi classes have two methods for each operation - one returning payload class, second returning ResponseEntity<> - which is reasonable for generated code, but also clutters the API.
  • it is yet another dependency that may have bugs, get out of date, be incompatible with newer Spring versions

Also, starting from Spring Framework 6, we can use Http Interface Clients which makes it quite trivial to implement HTTP clients by hand.

Conclusion

While I am not in love with the code generated by OpenAPI Generator, I believe it is worth to have such tool in your toolbox. You can find the complete project at: maciejwalkowiak/spring-boot-openapi-client-generator-demo for easy copy & pasting.

As usual, I hope you found it useful! Feel free to drop a comment if you found any mistake or have a question. Also, feel free to reach out to me on twitter.com/maciejwalkowiak.

Let's stay in touch and follow me on Twitter: @maciejwalkowiak

Subscribe to RSS feed