# 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`.

## Writing a Spring Boot application

You can find all the code for this step in the 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.

### Defining the main application configuration with annotations

The `BookstoreApplication` class is the entry point for the API, and it's also where we define the main OpenAPI documentation properties:

```java filename="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 org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@OpenAPIDefinition(
        info = @Info(
                title = "Bookstore API",
                version = "1.0.0",
                description = "This API provides endpoints to manage a bookstore's inventory of books and magazines, " +
                        "as well as customer orders. You can use it to browse publications, create orders, and track " +
                        "order status.",
                contact = @Contact(
                        name = "Bookstore API Support",
                        email = "api@bookstore.example.com",
                        url = "https://bookstore.example.com/support"
                ),
                license = @License(
                        name = "Apache 2.0",
                        url = "https://www.apache.org/licenses/LICENSE-2.0.html"
                )
        ),
        servers = {
                @Server(url = "https://api.bookstore.example.com", description = "Production server (uses live data)"),
                @Server(url = "http://localhost:8080", description = "Development server (uses test data)")
        }
)
public class BookstoreApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookstoreApplication.class, args);
    }
}
```

The `@OpenAPIDefinition` annotation populates the OpenAPI document with essential context for anyone who wants to use the API. The `title`, `version`, and `description` fields describe what the API does, its current state, and how it can be used.

The `@Server` annotation defines 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
- A localhost server at `http://localhost:8080` for testing with sample data

### Defining data models with annotations

Next, let's look at how you can use OpenAPI annotations to describe API data structures in the `Models.java` file:

```java filename="Models.java"
package com.bookstore;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.v3.oas.annotations.media.Schema;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Schema(description = "Base class for all publications")
@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        property = "type",
        visible = true
)
@JsonSubTypes({
        @JsonSubTypes.Type(value = Book.class, name = "BOOK"),
        @JsonSubTypes.Type(value = Magazine.class, name = "MAGAZINE")
})
public abstract class Publication {
    @Schema(description = "Unique identifier of the publication", example = "123e4567-e89b-12d3-a456-426614174000")
    protected String id;

    @Schema(description = "Title of the publication", example = "Spring Boot in Action")
    protected String title;

    @Schema(description = "Publication date in ISO format", example = "2023-05-15")
    protected String publicationDate;

    @Schema(description = "Price in USD", example = "29.99")
    protected float price;

    @JsonProperty("type")
    @Schema(description = "Type of publication", example = "BOOK", allowableValues = {"BOOK", "MAGAZINE"})
    protected abstract String getType();

    // Getters and setters omitted for brevity
}

@Schema(description = "Book publication with author and ISBN")
class Book extends Publication {
    @Schema(description = "Author of the book", example = "Craig Walls")
    private String author;

    @Schema(description = "ISBN of the book", example = "978-1617292545")
    private String isbn;

    @Override
    @JsonIgnore
    protected String getType() {
        return "BOOK";
    }

    // Constructor, getters, and setters omitted for brevity
}

@Schema(description = "Magazine publication with issue number and publisher")
class Magazine extends Publication {
    @Schema(description = "Issue number of the magazine", example = "42")
    private int issueNumber;

    @Schema(description = "Publisher of the magazine", example = "O'Reilly Media")
    private String publisher;

    @Override
    @JsonIgnore
    protected String getType() {
        return "MAGAZINE";
    }

    // Constructor, getters, and setters omitted for brevity
}
```

The `@Schema` annotation can be used at both the class and field levels:

- 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 and example values.

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, allowing endpoints to accept or return either type. API clients will include a `type` field set to either `BOOK` or `MAGAZINE` to identify the publication type.

Here's how we define an `Order` class that references the `Publication` schema:

```java filename="Models.java"
@Schema(description = "Customer order for publications")
class Order {
    @Schema(description = "Unique identifier of the order", example = "order-123456")
    private String id;

    @Schema(description = "Name of the customer who placed the order", example = "John Doe")
    private String customerName;

    @Schema(description = "Email of the customer", example = "john.doe@example.com")
    private String customerEmail;

    @Schema(description = "List of publications in the order")
    private List<Publication> items;

    @Schema(description = "Current status of the order", example = "PENDING")
    private OrderStatus status;

    // Constructor, getters, and setters omitted for brevity
}
```

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 defined earlier.

For the order status, we use an enumeration:

```java filename="Models.java"
@Schema(description = "Status of an order")
enum OrderStatus {
    PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED
}
```

This appears in the OpenAPI document as a string field with a set of allowed values.

Finally, we define an error response model:

```java filename="Models.java"
@Schema(description = "Error response with code and message")
class ErrorResponse {
    @Schema(description = "Error code", example = "NOT_FOUND")
    private String code;

    @Schema(description = "Error message", example = "Publication with ID 123 not found")
    private String message;

    // Constructor, getters, and setters omitted for brevity
}
```

### Defining API endpoints with annotations

Now, let's define the API endpoints in the `PublicationsController.java` file:

```java filename="PublicationsController.java"
package com.bookstore;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/publications")
@Tag(name = "Publications", description = "Operations for managing publications (books and magazines)")
public class PublicationsController {
    // Controller methods follow
}
```

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

For each endpoint method, we use annotations to document their purpose and responses:

```java filename="PublicationsController.java"  
@Operation(summary = "Get a publication by ID", description = "Returns a single publication (book or magazine)")
@ApiResponses(value = {
    @ApiResponse(responseCode = "200", description = "Successful operation",
                 content = @Content(schema = @Schema(oneOf = {Book.class, Magazine.class}))),
    @ApiResponse(responseCode = "404", description = "Publication not found",
                 content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@GetMapping("/{id}")
public ResponseEntity<?> getPublication(
    @Parameter(description = "ID of the publication to return", required = true)
    @PathVariable String id) {
    // Implementation omitted for brevity
}
```

The `@Operation` and `@ApiResponses` annotations document what the endpoint does and what responses to expect. For example, `getPublication` is annotated to show that it returns a publication successfully (`200` status) or returns an error (`404` status) when the publication isn't found.

The `@Parameter` annotation describes the requirements for input parameters, such as the ID path parameter in this example.

## Examining 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:

```bash filename="Terminal"
./mvnw clean install
./mvnw spring-boot:run
```

Download the OpenAPI document while running the application:

```bash filename="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 explore the generated OpenAPI document to see how the Spring Boot annotations translate into an OpenAPI specification.

### The OpenAPI Specification version information

The OpenAPI document begins with version information:

```yaml filename="openapi.yaml"
openapi: 3.1.0
```

This version is determined by the configuration in our `application.properties` file. It tells API consumers which version of the OpenAPI Specification to expect.

### API information

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

```yaml filename="openapi.yaml"
info:
  title: Bookstore API
  description: >-
    This API provides endpoints to manage a bookstore's inventory of books and
    magazines, as well as customer orders. You can use it to browse
    publications, create orders, and track order status.
  contact:
    name: Bookstore API Support
    url: https://bookstore.example.com/support
    email: api@bookstore.example.com
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0.html
  version: 1.0.0
```

Notice how each field in the Java 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 information

Server configurations defined with `@Server` annotations appear in the servers array:

```yaml filename="openapi.yaml"
servers:
  - url: https://api.bookstore.example.com
    description: Production server (uses live data)
  - url: http://localhost:8080
    description: Development server (uses test data)
```

### Polymorphic models

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:

```yaml filename="openapi.yaml"
Publication:
  type: object
  description: Base class for all publications
  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
    publicationDate:
      type: string
      description: Publication date in ISO format
      example: "2023-05-15"
    price:
      type: number
      format: float
      description: Price in USD
      example: 29.99
    type:
      type: string
      description: Type of publication
      example: BOOK
      enum:
        - BOOK
        - MAGAZINE
  required:
    - id
    - title
    - publicationDate
    - price
    - type
  discriminator:
    propertyName: type
    mapping:
      BOOK: "#/components/schemas/Book"
      MAGAZINE: "#/components/schemas/Magazine"
```

Key aspects to notice:

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

### API endpoints

Finally, let's examine how controller methods translate into API endpoints. Here's how the `getPublication` endpoint appears in the OpenAPI document:

```yaml filename="openapi.yaml"
/publications/{id}:
  get:
    tags:
      - Publications
    summary: Get a publication by ID
    description: Returns a single publication (book or magazine)
    operationId: getPublication
    parameters:
      - name: id
        in: path
        description: ID of the publication to return
        required: true
        schema:
          type: string
    responses:
      "200":
        description: Successful operation
        content:
          application/json:
            schema:
              type: object
              oneOf:
                - $ref: "#/components/schemas/Book"
                - $ref: "#/components/schemas/Magazine"
      "404":
        description: Publication not found
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ErrorResponse"
```

The mapping is clear:

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

## Creating 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:

```bash filename="Terminal"
speakeasy --version
```

Now, generate a TypeScript SDK using the following command:

```bash filename="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.

You'll see the steps taken by Speakeasy to create the SDK in the terminal:

```bash filename="Terminal"
│ Workflow - running
│ Workflow - success
│ └─Target: sdk - success
│   └─Source: Bookstore API - success
│     └─Validating Document - success
│     └─Diagnosing OpenAPI - success
│     └─Tracking OpenAPI Changes - success
│       └─Snapshotting OpenAPI Revision - success
│       └─Storing OpenAPI Revision - success
│   └─Validating gen.yaml - success
│   └─Generating Typescript SDK - success
│     └─Setup Environment - success
│     └─Load and Validate Document - success
│     └─Generate SDK - success
│     └─Compile SDK - success
│     └─Setup Environment - success
│     └─Load and Validate Document - success
│     └─Generate SDK - success
│   └─Generating Code Samples - success
│     └─Snapshotting Code Samples - success
│       └─Snapshotting Code Samples - success
│       └─Uploading Code Samples - success
```

Speakeasy [validates](/docs/sdks/core-concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal. The generated SDK will be saved as a folder in your project.

If you get ESLint styling errors, run the `speakeasy quickstart` command from outside your project.

Speakeasy also suggests improvements for your SDK using [Speakeasy Suggest](/docs/prep-openapi/maintenance), which is an AI-powered tool in Speakeasy Studio. You can see suggestions by opening the link to your Speakeasy Studio workspace in the terminal:

![Speakeasy Suggestions in Speakeasy Studio](/assets/openapi/springboot/speakeasy-suggestions.png)

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.

In the SDK `README.md` file, you'll find documentation about your Speakeasy SDK. TypeScript SDKs generated with Speakeasy include an installable [Model Context Protocol (MCP) server](/docs/standalone-mcp/build-server) where the various SDK methods are exposed as tools that AI applications can invoke.
Your SDK documentation includes instructions for installing the MCP server.

Note that the SDK is not ready for production use. To get it production-ready, follow the steps outlined in your Speakeasy workspace.

## Customizing the SDK

The example app added retry logic to the SDK's `listPublications` operation to handle network errors gracefully. This was done using one of [Speakeasy's OpenAPI extensions](/docs/speakeasy-reference/extensions), `x-speakeasy-retries`.

Instead of modifying the OpenAPI document directly, this extension was added to the Spring Boot controller, and the OpenAPI document and SDK were regenerated.

These imports were added to `src/main/java/com/bookstore/PublicationsController.java`:

```java filename="PublicationsController.java"
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
```

The `listPublications` operation was modified to include the retry configuration:

```java filename="PublicationsController.java" mark=2:7
    @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);
    }
```

The OpenAPI document was then regenerated:

```bash filename="Terminal"
curl http://localhost:8080/api-docs.yaml -o openapi.yaml
```

The OpenAPI document includes the retry configuration for the `listPublications` operation:

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

After recreating the SDK using Speakeasy:

```bash
speakeasy quickstart
```

The created SDK now includes 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@speakeasy.com](mailto:support@speakeasy.com).

If you haven't already, take a look at our [blog](/blog) to learn more about OpenAPI, SDK generation, and more, including:

- [Native JSONL support in your SDKs](/blog/release-jsonl-support)
- [Comprehensive SDK testing](/blog/release-sdk-testing)
- [Model Context Protocol: TypeScript SDKs for the agentic AI ecosystem](/blog/release-model-context-protocol)