Generating HTTP clients for Spring Boot application according to OpenAPI specification

By following this step-by-step tutorial, you will learn how to generate HTTP client code for a Spring Boot application according to the OpenAPI specification using the plugin. openapi-generator for Gradle.

OpenAPI has become a standard for defining and documenting HTTP-based RESTful APIs. While Spring Boot does not support OpenAPI out of the box, there are several third-party projects that fill this gap:

  • ePages-de/restdocs-api-spec – extension for Spring REST Docswhich adds the functionality of generating OpenAPI specifications

  • Springdoc – generates OpenAPI specifications at runtime, based on Spring MVC/Webflux controllers and project configuration

  • Springwolf – similar to Springdoc, but for asynchronous APIs generates AsyncAPI compatible specifications for integration with Kafka/RabbitMQ/JMS/SQS etc.

All three projects cover the server side of the API.

Since OpenAPI is a well-defined specification, it can also be used on the client side to generate client code. For this case, there is a project that is considered by the community as a de facto standard: https://openapi-generator.tech/.

OpenAPI Generator generates clients for various programming languages ​​and frameworks. Depending on the generator you choose, it can generate client or server code.

Today I will focus exclusively on generation clients for Spring Boot applications. I will also tell you how you can customize the generator to suit your needs.

Project setup

Let's start with an empty Spring Boot project created with start.spring.iowith Spring Web dependency selected and Gradle as the build system.

I'm going to generate HTTP clients for Spring Pet Clinic Restso I copied the file openapi.yml to the root directory of my project.

Installing OpenAPI Generator

The OpenAPI generator is available as a CLI, Maven or Gradle plugin. I will use Gradle, but the configuration can be easily adapted for the Maven plugin.

Set up org.openapi.generator with latest version in the build.gradle file:

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

After configuring the plugin, new Gradle tasks will be available:

$ ./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

Configuring the generator and library

Before you can use the generator, you need to set it up. First, select the generator from the list on official website (Note that the list contains separate sections for client, server, specification and configuration documentation.) Since we are going to generate client codewe will only be interested in the contents of the client section.

At the time of writing, there are three generators available for Java: java, java-helidon-client, and java-micronaut-client. Does this mean that there is no dedicated Spring client? Not really.

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

Important information

At the time of writing, there is no option to generate client code that uses @HttpExchange clients based on the declarative interfaces of Spring 6+ versions.

Let's take a “modern” approach and create a client based on RestClient:

IN build.gradle let's add the following section:

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

All possible configuration options for the java generator can be found in official documentation.

Setting up the specification location

OpenAPI Generator allows you to generate code based on specifications stored in local or remote JSON or YAML files.

If you have a local specification file, for example named petclinic-spec.ymllocated in the same directory as the file build.gradlespecify the path to this file in the property inputSpecas shown below:

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

To use a remotely located specification, OpenAPI Generator requires a URL instead of a local file path. In this case, instead of the inputSpec property, use remoteInputSpecspecifying a link to the remote file.

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

If I had a choice between JSON or YAML specs, I would choose JSON. I have had cases where YAML generated by other server-side tools was either invalid or not valid enough to be used in the OpenAPI generator. I have not had such problems with JSON.

Once we have set up the specification and the generator, we can call ./gradlew openApiGenerate to generate client code. The generated code will appear in build/generate-resources/main, but its quality may leave much to be desired.

I was expecting to generate only the model classes, their requests/responses mapping, and a class representing the API. But the generator created a whole Maven/Gradle project. It included CI configurations, documentation, Android Manifest, and more. This may not be important to you, since you are not going to save this code to a Git repository. But for me, it is a problem: too many unnecessary classes that will not be used in the classpath. Let's simplify the project and leave only the really necessary code.

Remove unnecessary code

OpenAPI generator supports file ignoresimilar .gitignorewhich allows you to specify which files should be excluded when generating a project. Thanks to this, we can filter out unnecessary files and leave only what is really necessary.

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

Let's create a file .openapi-generator-java-sources.ignore in the root directory of the project with the following contents:

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

Now if you generate the project again using the command ./gradlew openApiGenerateyou will see much less unnecessary files:

While there may still be some classes you want to exclude, I'll leave that up to you. For me, this is a perfectly acceptable result.

Configure the package name

You may have noticed that the classes are generated in the org.openapitools.client package. To change the package name, set the following properties:

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

Include generated sources into the project

By default, Gradle doesn't know the location of the generated code. To use it in your project, add it to SourceSet:

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

Now when you run ./gradlew buildboth your own code and the generated code will be compiled. You may be surprised, but compilation may fail.

Exclude Jackson Nullable module

The generated code depends on the module jackson-databind-nullable. Since we removed the generated pom.xml And build.gradle files, and left only Java classes, we have two options: add a dependency on this module or configure the OpenAPI plugin so that it does not use this module. I choose the second option.

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

Configure openApiGenerate to run automatically before each compilation

I don't want to have to remember to re-generate classes after every change, so I'll configure Gradle to automatically re-generate classes every time I run build or compile my code:

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

What we got

Here is the final version of the file build.gradle with all the changes mentioned. You can simply copy and paste it:

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')
}

Setting up Spring beans

The generated code consists of three main parts:

  1. ApiClient – a low-level generic HTTP client that uses Spring's RestClient. This component is not used directly in our code.

  2. PetApi, OwnerApi and others – high-level clients containing all operations for each OpenAPI tag.

  3. Model classes – classes that display JSON requests and responses.

None of these classes are marked with Spring annotations such as @Component or @Serviceso the beans will have to be created using Java configuration.

Because ApiClient uses RestClientwe can (and most likely should) pass the automatically configured RestClient.Builder from Spring to constructor ApiClientto take advantage of auto-configuration RestClient.

@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);
}

Setting the base URL

The base URL can be set for either RestClient with subsequent transfer to ApiClientand directly for ApiClient through the method setBasePath. It would be more logical from Spring's point of view to set the URL for RestClientbut unfortunately this option will not work. ApiClient does not accept the base URL set for RestClientso the base path must be set directly for ApiClient.

Configuring authentication

If an API requires authentication, a similar question arises: where exactly to configure it. The answer depends on whether the specification includes authentication requirements or not.

Configuring authentication for ApiClient

If the API specification file contains authentication information, such as basic authentication:

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

In this case, ApiClient will apply authentication headers only to those operations that require authentication:

@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;
}

Configuring authentication for RestClient

If the API specification file does not contain authentication information, but the API still requires authentication, then authentication can be configured at the RestClient:

@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 this case, the authentication header will be sent in response to all requests made through ApiClient.

Advantages and disadvantages

The most obvious benefits of code generation for the client are:

  • No need to write code manually, which saves time and effort

  • Automatic code generation reduces the likelihood of errors such as incorrect URLs or invalid data.

  • It's easier to track and adapt to API changes

As usual, there are some drawbacks – the generated code is completely different from the code I would write myself:

  • OpenAPI generates mutable classes for requests and responses, although they should be immutable.

  • The generator does not use Java records (there are reasons and a workaround for that)

  • All XXXApi classes have two methods for each operation: one returns a class representing the data, the other returns a ResponseEntity<>. This makes sense for the generated code, but at the same time clutters the API.

  • This is another dependency that may be buggy, out of date, or incompatible with newer versions of Spring.

Additionally, starting with Spring Framework 6, you can use Http Interface Clientswhich greatly simplifies the implementation of HTTP clients manually.

Conclusion

While I don't particularly like the code generated by OpenAPI Generator, I think it's a tool worth having in your arsenal. You can find the full project at GitHubso you can easily copy and paste its contents.

Join the Russian-speaking Spring Boot developer community on Telegram — Spring AIOto stay up to date with the latest news from the world of Spring Boot development and everything related to it.

Waiting for everybody, join us!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *