SDA Commons Server OpenAPI
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:
| 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)
.
Swagger-Core Annotations
Best Practices in API Documentation
Example
config.yml
-
server.rootPath
| 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
| 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.