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.
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:
<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:
# Specify the path of the OpenAPI documentationspringdoc.api-docs.path=/api-docsspringdoc.api-docs.version=OPENAPI_3_1# Specify the path of the Swagger UIspringdoc.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.
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:
./mvnw clean install./mvnw spring-boot:run
Download the OpenAPI document while running the application:
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: 3.1.0info:title: Book Store APIdescription: API for managing a book store inventory and orderstermsOfService: https://bookstore.example.com/termscontact:name: API Supporturl: https://bookstore.example.com/supportemail: support@bookstore.example.comlicense:name: Apache 2.0url: https://www.apache.org/licenses/LICENSE-2.0.htmlversion: 1.0.0servers:- url: http://localhost:8080description: Local server- url: https://api.bookstore.example.comdescription: Production servertags:- name: Ordersdescription: Order management APIs- name: Publicationsdescription: Publications management APIspaths:/orders/{id}/status:put:tags:- Orderssummary: Update order statusdescription: Update the status of an existing orderoperationId: updateOrderStatusparameters:- name: idin: pathdescription: ID of the order to updaterequired: trueschema:type: string- name: statusin: querydescription: New status for the orderrequired: trueschema:type: stringdescription: Possible statuses for an orderenum:- PENDING- SHIPPED- DELIVERED- CANCELLEDresponses:"200":description: Order status updated successfullycontent:application/json:schema:$ref: "#/components/schemas/Order""404":description: Order not foundcontent:'*/*':schema:$ref: "#/components/schemas/ErrorResponse""400":description: Invalid statuscontent:'*/*':schema:$ref: "#/components/schemas/ErrorResponse"/publications:get:tags:- Publicationssummary: List all publicationsdescription: Get a list of all publications in the storeoperationId: listPublicationsresponses:"200":description: Successful operationcontent:'*/*':schema:type: arrayitems:$ref: "#/components/schemas/PublicationListItem"x-speakeasy-retries:statusCodes:- 5XXbackoff:initialInterval: 500maxInterval: 60000maxElapsedTime: 3600000exponent: 1.5strategy: backoffretryConnectionErrors: truepost:tags:- Publicationssummary: Create a new publicationdescription: Add a new publication to the storeoperationId: createPublicationrequestBody:content:application/json:schema:oneOf:- $ref: "#/components/schemas/Book"- $ref: "#/components/schemas/Magazine"required: trueresponses:"400":description: Invalid inputcontent:'*/*':schema:$ref: "#/components/schemas/ErrorResponse""200":description: Successful operationcontent:'*/*':schema:oneOf:- $ref: "#/components/schemas/Book"- $ref: "#/components/schemas/Magazine"/orders:get:tags:- Orderssummary: List all ordersdescription: Get a list of all orders in the systemoperationId: listOrdersresponses:"200":description: Successful operationcontent:'*/*':schema:type: arrayitems:$ref: "#/components/schemas/Order"post:tags:- Orderssummary: Create a new orderdescription: Create a new order for publicationsoperationId: createOrderrequestBody:content:application/json:schema:$ref: "#/components/schemas/Order"required: trueresponses:"400":description: Invalid inputcontent:'*/*':schema:$ref: "#/components/schemas/ErrorResponse""200":description: Order created successfullycontent:application/json:schema:$ref: "#/components/schemas/Order"/orders/{id}/cancel:post:tags:- Orderssummary: Cancel an orderdescription: Cancel an existing orderoperationId: cancelOrderparameters:- name: idin: pathdescription: ID of the order to cancelrequired: trueschema:type: stringresponses:"404":description: Order not foundcontent:'*/*':schema:$ref: "#/components/schemas/ErrorResponse""200":description: Order cancelled successfullycontent:'*/*':schema:$ref: "#/components/schemas/Order""400":description: Order cannot be cancelledcontent:'*/*':schema:$ref: "#/components/schemas/ErrorResponse"/publications/{id}:get:tags:- Publicationssummary: Get a publication by IDdescription: Retrieves a publication's details by its unique identifieroperationId: getPublicationparameters:- name: idin: pathdescription: ID of the publication to retrieverequired: trueschema:type: stringresponses:"404":description: Publication not foundcontent:'*/*':schema:$ref: "#/components/schemas/ErrorResponse""200":description: Successful operationcontent:'*/*':schema:oneOf:- $ref: "#/components/schemas/Book"- $ref: "#/components/schemas/Magazine"/orders/{id}:get:tags:- Orderssummary: Get an order by IDdescription: Retrieves an order's details by its IDoperationId: getOrderparameters:- name: idin: pathdescription: ID of the order to retrieverequired: trueschema:type: stringresponses:"200":description: Successful operationcontent:'*/*':schema:$ref: "#/components/schemas/Order""404":description: Order not foundcontent:'*/*':schema:$ref: "#/components/schemas/ErrorResponse"components:schemas:Book:allOf:- $ref: "#/components/schemas/Publication"- type: objectproperties:type:type: stringdefault: Bookdescription: Type of the publicationenum:- Bookexample: BookreadOnly: trueauthor:type: stringdescription: Author of the bookexample: Craig Wallsisbn:type: stringdescription: ISBN of the bookexample: 978-1617292545description: Represents a book in the storerequired:- typeMagazine:allOf:- $ref: "#/components/schemas/Publication"- type: objectproperties:type:type: stringdefault: Magazinedescription: Type of the publicationenum:- Magazineexample: MagazinereadOnly: trueissueNumber:type: integerformat: int32description: Issue number of the magazineexample: 42publisher:type: stringdescription: Publisher of the magazineexample: National Geographic Societydescription: Represents a magazine in the storerequired:- typeOrder:description: Represents an order for publicationsproperties:id:type: stringdescription: Unique identifier for the orderexample: ord-123456customerId:type: stringdescription: Customer who placed the orderexample: cust-789012items:type: arraydescription: List of items in the orderitems:$ref: "#/components/schemas/PublicationListItem"minItems: 1totalPrice:type: numberformat: floatdescription: Total price of the orderexample: 89.97status:type: stringdescription: Possible statuses for an orderenum:- PENDING- SHIPPED- DELIVERED- CANCELLEDPublication:description: Represents a publication in the storediscriminator:propertyName: typemapping:Book: "#/components/schemas/Book"Magazine: "#/components/schemas/Magazine"properties:id:type: stringdescription: Unique identifier of the publicationexample: 123e4567-e89b-12d3-a456-426614174000title:type: stringdescription: Title of the publicationexample: Spring Boot in ActionpublishDate:type: stringdescription: Publication dateexample: 2023-06-15price:type: numberformat: floatdescription: Price in USDexample: 39.99type:type: stringdescription: Type of the publicationenum:- Book- Magazineexample: BookPublicationListItem:allOf:- $ref: "#/components/schemas/Publication"description: Represents an item in a list of publicationsdiscriminator:propertyName: typemapping:Book: "#/components/schemas/Book"Magazine: "#/components/schemas/Magazine"oneOf:- $ref: "#/components/schemas/Book"- $ref: "#/components/schemas/Magazine"ErrorResponse:description: Represents an error responseproperties:code:type: integerformat: int32description: Error codeexample: 404message:type: stringdescription: Error messageexample: 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:
speakeasy --version
Now, generate a TypeScript SDK using the following command:
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
:
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:
@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)))),})@GetMappingpublic 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:
curl http://localhost:8080/api-docs.yaml -o openapi.yaml
The OpenAPI document will include the retry configuration for the listPublications
operation:
x-speakeasy-retries:statusCodes:- 5XXbackoff:initialInterval: 500maxInterval: 60000maxElapsedTime: 3600000exponent: 1.5strategy: backoffretryConnectionErrors: 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.