Generating an OpenAPI document and SDK from Spring Boot

You have a Spring Boot API and need to generate SDKs or API documentation for other teams. Rather than writing and maintaining separate OpenAPI specs, we will walk through how to generate them directly from your Spring Boot code, and then use them to create and customize an SDK.

We’ll work with real code you can run locally, building a simple bookstore API to demonstrate how to properly document API structures, including inheritance between models, endpoint definitions, response types, and error handling. The examples illustrate how Spring Boot annotations map to OpenAPI concepts, so you can see how your code translates into API specifications.

Info Icon

Example repository

The example below will guide you through the process of creating a Spring Boot project, adding the necessary dependencies, writing Spring Boot controllers with OpenAPI annotations, and generating an OpenAPI document from it. To skip this setup and follow along with our example, clone the example application (opens in a new tab).

Step 1: Set up a Spring Boot project

First, create a new Spring Boot project using Spring Initializr (opens in a new tab). Select the following options:

  • Project: Maven
  • Language: Java
  • Spring Boot: 2.7.x (or the latest stable version)
  • Project Metadata: Fill in as appropriate
  • Dependencies: Spring Web

Download the project and extract it to your preferred directory.

Step 2: Add OpenAPI dependencies

Open the pom.xml file and add the following dependency:

pom.xml
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>

Step 3: Configure application properties

Open the src/main/resources/application.properties file and add the following configuration:

application.properties
# Specify the path of the OpenAPI documentation
springdoc.api-docs.path=/api-docs
springdoc.api-docs.version=OPENAPI_3_1
# Specify the path of the Swagger UI
springdoc.swagger-ui.path=/swagger-ui.html

These properties configure the application name that identifies your service, the endpoint where the OpenAPI document will be available (/api-docs), the version of the OpenAPI document to generate, and the URL path where you can access the Swagger UI documentation (/swagger-ui.html).

After starting your application, you can view the OpenAPI document at http://localhost:8080/api-docs and access the interactive Swagger UI at http://localhost:8080/swagger-ui.html.

Step 4: Write a Spring Boot application

All the code in this step can be found in our example application.

Open the src/main/java/com/bookstore/BookstoreApplication.java file in your text editor to see where to begin when adding OpenAPI annotations to your project.

The BookstoreApplication class is the entry point for the API. Similarly, we define the entry point to the OpenAPI document.


The @OpenAPIDefinition annotation populates the OpenAPI document with essential context for anyone who wants to use the API. This tells developers what the API does and how they can use it.

Use the title, version, and description fields to describe what the API does, its current state, and how it can be used.


The @Server annotation defines any available endpoints for the API. In the example, there are two options: A production server at https://api.bookstore.example.com that uses live data, and a localhost server at http://localhost:8080 for testing with sample data.


Open the Models.java file to see how you can use OpenAPI annotations to describe API data structures.


The @Schema annotation can be used at both the class and field levels to describe data structures in the OpenAPI documentation.

At the class level, @Schema describes what a Publication, Book, or Magazine represents in the API.

At the field level, fields like id and author are documented with a description of the field’s purpose and an example of the value that API users should provide or expect to receive.


The Publication class acts as the base schema in the OpenAPI specification.

By using @JsonTypeInfo and @JsonSubTypes, we tell OpenAPI that a Publication can be either a Book or Magazine. This polymorphism is reflected in the OpenAPI document as a oneOf schema, where each endpoint that works with publications can accept or return either type.

API clients will see that when working with publications, they need to include a type field set to either BOOK or MAGAZINE to properly identify the publication type.


The Order class uses the @Schema annotation to document the items field, which references the Publication schema.

This tells OpenAPI that Orders can contain an array of either books or magazines, using the polymorphic structure we defined earlier.


To document the available statuses of an Order, we annotate an enum with @Schema.

In the OpenAPI specification, this appears as a string field specifying a set of allowed values.


We annotate the ErrorResponse class with @Schema to describe any errors users might run into when using the API endpoints.


The next step is to define the API endpoints. We will go through the PublicationsController.java file to see how to map a Spring Boot controller to OpenAPI.


The @Tag annotation groups the operations in the controller under “Publications” in the OpenAPI document. Combined with @RequestMapping("/publications"), it tells API consumers that these endpoints handle publication-related operations.


On each method, you can use the @Operation and @ApiResponses annotations to document what the endpoint does and what responses to expect.

For example, getPublication has been annotated to show that it returns a publication successfully (200 status) or an error (404 status) if the publication isn’t found.


Use the @Parameter annotation to describe the requirements for input parameters.

BookstoreApplication.java
package com.bookstore;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.models.annotations.OpenAPI31;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@OpenAPI31
@OpenAPIDefinition(
info = @Info(
title = "Book Store API",
version = "1.0.0",
description = "API for managing a book store inventory and orders",
termsOfService = "https://bookstore.example.com/terms",
contact = @Contact(
name = "API Support",
url = "https://bookstore.example.com/support",
email = "support@bookstore.example.com"
),
license = @License(
name = "Apache 2.0",
url = "https://www.apache.org/licenses/LICENSE-2.0.html"
)
),
servers = {
@Server(url = "http://localhost:8080", description = "Local server"),
@Server(url = "https://api.bookstore.example.com", description = "Production server")
}
)
public class BookstoreApplication {
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class, args);
}
}

Step 5: View the generated OpenAPI document

Now that we’ve built the Spring Boot application, let’s generate and examine the OpenAPI document to understand how the Java code translates into API specifications.

First, install the necessary dependencies in the project and start the application with the following commands:

Terminal
./mvnw clean install
./mvnw spring-boot:run

Download the OpenAPI document while running the application:

Terminal
curl http://localhost:8080/api-docs.yaml -o openapi.yaml

This command saves the OpenAPI document as openapi.yaml in your current directory.

Let’s scroll through the generated OpenAPI document to see how our Spring Boot annotations were translated into an OpenAPI specification.


The OpenAPI document begins with version information.

This version is determined by the springdoc-openapi library we’re using. Simple, but important - it tells API consumers which OpenAPI spec version to expect.


Next comes the info object, which is generated from the @OpenAPIDefinition annotation.

This Java annotation transforms directly into the corresponding OpenAPI structure.

Notice how each field in the annotation maps directly to its counterpart in the OpenAPI document output. This one-to-one mapping makes it easy to understand how your code affects the final API documentation.


Server configurations defined with @Server annotations appear in the servers array.

These annotations generate the following OpenAPI structure.


One of the more complex aspects of the API is how polymorphic models are represented.

The Publication class has been translated into a schema that supports polymorphism through a discriminator.

A few key things to notice:

  • The @Schema annotations provide descriptions and examples
  • The @JsonTypeInfo annotation determines the discriminator property
  • The @JsonSubTypes annotation defines the possible concrete implementations

Finally, let’s examine how controller methods translate into API endpoints.

The @Operation annotation provides the summary and description.

Each @ApiResponse maps to an entry in the responses object.

The @Parameter annotation documents the path parameter.

openapi.yaml
openapi: 3.1.0
info:
title: Book Store API
description: API for managing a book store inventory and orders
termsOfService: https://bookstore.example.com/terms
contact:
name: API Support
url: https://bookstore.example.com/support
email: support@bookstore.example.com
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
version: 1.0.0
servers:
- url: http://localhost:8080
description: Local server
- url: https://api.bookstore.example.com
description: Production server
tags:
- name: Orders
description: Order management APIs
- name: Publications
description: Publications management APIs
paths:
/orders/{id}/status:
put:
tags:
- Orders
summary: Update order status
description: Update the status of an existing order
operationId: updateOrderStatus
parameters:
- name: id
in: path
description: ID of the order to update
required: true
schema:
type: string
- name: status
in: query
description: New status for the order
required: true
schema:
type: string
description: Possible statuses for an order
enum:
- PENDING
- SHIPPED
- DELIVERED
- CANCELLED
responses:
"200":
description: Order status updated successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"404":
description: Order not found
content:
'*/*':
schema:
$ref: "#/components/schemas/ErrorResponse"
"400":
description: Invalid status
content:
'*/*':
schema:
$ref: "#/components/schemas/ErrorResponse"
/publications:
get:
tags:
- Publications
summary: List all publications
description: Get a list of all publications in the store
operationId: listPublications
responses:
"200":
description: Successful operation
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/PublicationListItem"
x-speakeasy-retries:
statusCodes:
- 5XX
backoff:
initialInterval: 500
maxInterval: 60000
maxElapsedTime: 3600000
exponent: 1.5
strategy: backoff
retryConnectionErrors: true
post:
tags:
- Publications
summary: Create a new publication
description: Add a new publication to the store
operationId: createPublication
requestBody:
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/Book"
- $ref: "#/components/schemas/Magazine"
required: true
responses:
"400":
description: Invalid input
content:
'*/*':
schema:
$ref: "#/components/schemas/ErrorResponse"
"200":
description: Successful operation
content:
'*/*':
schema:
oneOf:
- $ref: "#/components/schemas/Book"
- $ref: "#/components/schemas/Magazine"
/orders:
get:
tags:
- Orders
summary: List all orders
description: Get a list of all orders in the system
operationId: listOrders
responses:
"200":
description: Successful operation
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/Order"
post:
tags:
- Orders
summary: Create a new order
description: Create a new order for publications
operationId: createOrder
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
required: true
responses:
"400":
description: Invalid input
content:
'*/*':
schema:
$ref: "#/components/schemas/ErrorResponse"
"200":
description: Order created successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
/orders/{id}/cancel:
post:
tags:
- Orders
summary: Cancel an order
description: Cancel an existing order
operationId: cancelOrder
parameters:
- name: id
in: path
description: ID of the order to cancel
required: true
schema:
type: string
responses:
"404":
description: Order not found
content:
'*/*':
schema:
$ref: "#/components/schemas/ErrorResponse"
"200":
description: Order cancelled successfully
content:
'*/*':
schema:
$ref: "#/components/schemas/Order"
"400":
description: Order cannot be cancelled
content:
'*/*':
schema:
$ref: "#/components/schemas/ErrorResponse"
/publications/{id}:
get:
tags:
- Publications
summary: Get a publication by ID
description: Retrieves a publication's details by its unique identifier
operationId: getPublication
parameters:
- name: id
in: path
description: ID of the publication to retrieve
required: true
schema:
type: string
responses:
"404":
description: Publication not found
content:
'*/*':
schema:
$ref: "#/components/schemas/ErrorResponse"
"200":
description: Successful operation
content:
'*/*':
schema:
oneOf:
- $ref: "#/components/schemas/Book"
- $ref: "#/components/schemas/Magazine"
/orders/{id}:
get:
tags:
- Orders
summary: Get an order by ID
description: Retrieves an order's details by its ID
operationId: getOrder
parameters:
- name: id
in: path
description: ID of the order to retrieve
required: true
schema:
type: string
responses:
"200":
description: Successful operation
content:
'*/*':
schema:
$ref: "#/components/schemas/Order"
"404":
description: Order not found
content:
'*/*':
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
schemas:
Book:
allOf:
- $ref: "#/components/schemas/Publication"
- type: object
properties:
type:
type: string
default: Book
description: Type of the publication
enum:
- Book
example: Book
readOnly: true
author:
type: string
description: Author of the book
example: Craig Walls
isbn:
type: string
description: ISBN of the book
example: 978-1617292545
description: Represents a book in the store
required:
- type
Magazine:
allOf:
- $ref: "#/components/schemas/Publication"
- type: object
properties:
type:
type: string
default: Magazine
description: Type of the publication
enum:
- Magazine
example: Magazine
readOnly: true
issueNumber:
type: integer
format: int32
description: Issue number of the magazine
example: 42
publisher:
type: string
description: Publisher of the magazine
example: National Geographic Society
description: Represents a magazine in the store
required:
- type
Order:
description: Represents an order for publications
properties:
id:
type: string
description: Unique identifier for the order
example: ord-123456
customerId:
type: string
description: Customer who placed the order
example: cust-789012
items:
type: array
description: List of items in the order
items:
$ref: "#/components/schemas/PublicationListItem"
minItems: 1
totalPrice:
type: number
format: float
description: Total price of the order
example: 89.97
status:
type: string
description: Possible statuses for an order
enum:
- PENDING
- SHIPPED
- DELIVERED
- CANCELLED
Publication:
description: Represents a publication in the store
discriminator:
propertyName: type
mapping:
Book: "#/components/schemas/Book"
Magazine: "#/components/schemas/Magazine"
properties:
id:
type: string
description: Unique identifier of the publication
example: 123e4567-e89b-12d3-a456-426614174000
title:
type: string
description: Title of the publication
example: Spring Boot in Action
publishDate:
type: string
description: Publication date
example: 2023-06-15
price:
type: number
format: float
description: Price in USD
example: 39.99
type:
type: string
description: Type of the publication
enum:
- Book
- Magazine
example: Book
PublicationListItem:
allOf:
- $ref: "#/components/schemas/Publication"
description: Represents an item in a list of publications
discriminator:
propertyName: type
mapping:
Book: "#/components/schemas/Book"
Magazine: "#/components/schemas/Magazine"
oneOf:
- $ref: "#/components/schemas/Book"
- $ref: "#/components/schemas/Magazine"
ErrorResponse:
description: Represents an error response
properties:
code:
type: integer
format: int32
description: Error code
example: 404
message:
type: string
description: Error message
example: Publication not found

Create an SDK from the OpenAPI document

Now that we have an OpenAPI document for the Spring Boot API, we can create an SDK using Speakeasy.

First, make sure you have Speakeasy installed:

Terminal
speakeasy --version

Now, generate a TypeScript SDK using the following command:

Terminal
speakeasy quickstart

Follow the onscreen prompts to provide the configuration details for the new SDK such as the name, schema location, and output path. Enter openapi.yaml when prompted for the OpenAPI document location and select your preferred language, for example, TypeScript, when prompted for which language you would like to generate.

After running this command, you’ll find the generated SDK code in the specified output directory. This SDK can be used by clients to interact with your Spring Boot API in a type-safe manner.

Customize the SDK

Let’s add retry logic to the SDK’s listPublications operation to handle network errors gracefully. We’ll do this using an OpenAPI extension that Speakeasy provides (opens in a new tab), x-speakeasy-retries.

Instead of modifying the OpenAPI document directly, we’ll add this extension to the Spring Boot controller and regenerate the OpenAPI document and SDK.

First, add these imports to src/main/java/com/bookstore/PublicationsController.java:

PublicationsController.java
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;

Then modify the listPublications operation to include the retry configuration:

PublicationsController.java
@Operation(summary = "List all publications", description = "Get a list of all publications in the store", extensions = {
@Extension(name = "x-speakeasy-retries", properties = {
@ExtensionProperty(name = "strategy", value = "backoff"),
@ExtensionProperty(name = "backoff", parseValue = true, value = "{\"initialInterval\":500,\"maxInterval\":60000,\"maxElapsedTime\":3600000,\"exponent\":1.5}"),
@ExtensionProperty(name = "statusCodes", parseValue = true, value = "[\"5XX\"]"),
@ExtensionProperty(name = "retryConnectionErrors", parseValue = true, value = "true")
})
})
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful operation",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PublicationListItem.class)))),
})
@GetMapping
public ResponseEntity<List<Publication>> listPublications() {
// This is a mock implementation. In a real application, you would fetch this from a database.
List<Publication> publications = new ArrayList<>();
publications.add(new Book(UUID.randomUUID().toString(), "Spring Boot in Action", "2015-10-01", 39.99f, "Craig Walls", "978-1617292545"));
publications.add(new Magazine(UUID.randomUUID().toString(), "National Geographic", "2023-06-01", 9.99f, 6, "National Geographic Society"));
return ResponseEntity.ok(publications);
}

Now that we’ve added the x-speakeasy-retries extension to the listPublications operation, we can regenerate the OpenAPI document:

Terminal
curl http://localhost:8080/api-docs.yaml -o openapi.yaml

The OpenAPI document will include the retry configuration for the listPublications operation:

openapi.yaml
x-speakeasy-retries:
statusCodes:
- 5XX
backoff:
initialInterval: 500
maxInterval: 60000
maxElapsedTime: 3600000
exponent: 1.5
strategy: backoff
retryConnectionErrors: true

Now we can use Speakeasy to recreate the SDK:

speakeasy quickstart

The created SDK will now include retry logic for the listPublications operation, automatically handling network errors and 5XX responses.

Issues and feedback

Need some assistance or have a suggestion? Reach out to our team at support@speakeasyapi.dev.