Skip to content

SDA Commons Server OpenAPI

javadoc

The module sda-commons-server-openapi is the base module to add OpenAPI support for applications in the SDA infrastructure. This package produces OpenApi 3.0 definitions.

Usage

In the application class, the bundle is added in the initialize method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class ExampleApp extends Application<Configuration> {

  // ...

  @Override
  public void initialize(Bootstrap<Configuration> bootstrap) {
    // ...
    bootstrap.addBundle(
      OpenApiBundle.builder()
        .addResourcePackageClass(getClass())
        .build());
    // ...
  }
}

The above will scan resources in the package of the application class. Customize the OpenAPI definition with the OpenAPIDefinition on a class in the registered package or use a configuration file.

Documentation Location

The OpenAPI documentation base path is dependent on DropWizard's server.rootPath:

  • as JSON: <server.rootPath>/openapi.json
  • as YAML: <server.rootPath>/openapi.yaml

Customization Options

The packages scanned by OpenAPI:

1
2
3
4
5
OpenApiBundle.builder()
//...
    .addResourcePackageClass(getClass())
    .addResourcePackageClass(Api.class)
    .addResourcePackage("my.package.containing.resources")

File Generation

To automatically generate the OpenAPI spec and ensure that it is committed to version control, one can use a test like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import static org.assertj.core.api.Assertions.assertThat;
import static org.sdase.commons.server.openapi.OpenApiFileHelper.normalizeOpenApiYaml;

import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.jupiter.api.Test;
import org.sdase.commons.server.testing.GoldenFileAssertions;

class OpenApiDocumentationTest {

  @Test
  void shouldHaveSameOpenApiInRepository() throws Exception {
    // receive the openapi.yaml from the OpenApiBundle
    var bundle =
        OpenApiBundle.builder()
            .addResourcePackage(YourApplication.class.getPackageName())
            .build();
    var openapi = bundle.generateOpenApiAsYaml();
    assertThat(openapi).isNotNull();

    // specify where you want your file to be stored
    Path filePath = Paths.get("openapi.yaml");

    // check and update the file
    GoldenFileAssertions.assertThat(filePath)
        .hasYamlContentAndUpdateGolden(normalizeOpenApiYaml(openapi));
  }
}

This test uses the GoldenFileAssertions from sda-commons-server-testing and removes all contents that vary between tests (the servers key that contains random port numbers) with OpenApiFileHelper#nomalizeOpenApiYaml(String yaml).

Further Information

Swagger-Core Annotations

Best Practices in API Documentation

Example

config.yml - server.rootPath

1
2
server:
  rootPath: "/api/*"

ExampleApp.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package org.example.person.app;

import org.example.person.api.Api;
//...

public class ExampleApp extends Application<Configuration> {

  // ...

  @Override
  public void initialize(Bootstrap<Configuration> bootstrap) {
    // ...
    bootstrap.addBundle(
      OpenApiBundle.builder()
        .addResourcePackageClass(Api.class)
        .build());
    // ...
  }
}

Api.java - @OpenAPIDefinition

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package org.example.person.api;

/// ...

@OpenAPIDefinition(
    info =
        @Info(
            title = "Example Api",
            version = "1.2",
            description = "Example Description",
            license = @License(name = "Apache License", url = "https://www.apache.org/licenses/LICENSE-2.0.html"),
            contact = @Contact(name = "John Doe", email = "john.doe@example.com")
        )
)
@SecurityScheme(
    type = SecuritySchemeType.HTTP,
    description = "Passes the Bearer Token (SDA JWT) to the service class.",
    name = "BEARER_TOKEN",
    scheme = "bearer",
    bearerFormat = "JWT")
public class Api {}

PersonService.java - @Operation, @ApiResponse, @Content, @Schema @SecurityRequirement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package org.example.person.api;

//...

@Path("/persons")
public interface PersonService {

  @GET
  @Path("/john-doe")
  @Produces(APPLICATION_JSON)
  @Operation(summary = "Returns John Doe.", security = {@SecurityRequirement(name = "BEARER_TOKEN")})
  @ApiResponse(
      responseCode = "200",
      description = "Returns John Doe.",
      content = @Content(schema = @Schema(implementation = PersonResource.class)))
  @ApiResponse(
      responseCode = "404",
      description = "John Doe was not found.")
  PersonResource getJohnDoe();
}

PersonResource.java - @Schema

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Resource
@Schema(description = "Person")
public class PersonResource {

   @Schema(description = "The person's first name.")
   private final String firstName;

   @Schema(description = "The person's last name.")
   private final String lastName;

   @Schema(description = "traits", example = "[\"hipster\", \"generous\"]")
   private final List<String> traits = new ArrayList<>();

   @JsonCreator
   public PersonResource(
         @JsonProperty("firstName") String firstName,
         @JsonProperty("lastName") String lastName,
         @JsonProperty("traits") List<String> traits) {

      this.firstName = firstName;
      this.lastName = lastName;
      this.traits.addAll(traits);
   }

   public String getFirstName() {
      return firstName;
   }

   public String getLastName() {
      return lastName;
   }

   public List<String> getTraits() {
      return traits;
   }
}

The generated documentation would be at:

  • as JSON: /api/openapi.json
  • as YAML: /api/openapi.yaml

Handling example values

The OpenApiBundle reads example annotations containing complex JSON instead of interpreting them as String. If the bundle encounters a value that could be interpreted as JSON, the value is parsed. If the value isn't JSON the value is interpreted as a string. If the example is supplied like example = "{\"key\": false}" the swagger definition will contain the example as example: {"key": false}.

Use an existing OpenAPI file

When working with the API first approach, it is possible to serve an existing OpenAPI file instead of generating it using Annotations. It is also possible to combine pre-existing and generated results into one file.

custom-openapi.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
openapi: 3.0.1
info:
  title: A manually written OpenAPI file
  description: This is an example file that was written by hand
  contact:
    email: info@sda.se
  version: '1.1'
paths:
  /house:
    # this path will be added
    put:
      summary: Update a house
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/House'
      responses:
        "201":
          description: The house has been updated
...

MyApplication.java

1
2
3
4
5
6
7
8
bootstrap.addBundle(
    OpenApiBundle.builder()
        // optionally configure other resource packages. Note that the values from annotations will
        // override the settings from the imported openapi file.
        .addResourcePackageClass(getClass())
        // provide the path to the existing openapi file (yaml or json) in the classpath
        .withExistingOpenAPIFromClasspathResource("/custom-openapi.yaml")
        .build());

Note: Annotations such as @OpenAPIDefinition will override the contents of the provided OpenAPI file if they are found in a configured resource package.