Documenting RESTful APIs

duration 20 minutes

Prerequisites:

Explore how to document and filter RESTful APIs from code or static files by using MicroProfile OpenAPI.

What you’ll learn

You will learn how to document and filter RESTful APIs from annotations, POJOs, and static OpenAPI files by using MicroProfile OpenAPI.

The OpenAPI specification, previously known as the Swagger specification, defines a standard interface for documenting and exposing RESTful APIs. This specification allows both humans and computers to understand or process the functionalities of services without requiring direct access to underlying source code or documentation. The MicroProfile OpenAPI specification provides a set of Java interfaces and programming models that allow Java developers to natively produce OpenAPI v3 documents from their JAX-RS applications.

You will document the RESTful APIs of the provided inventory service, which serves two endpoints, inventory/systems and inventory/properties. These two endpoints function the same way as in the other MicroProfile guides.

Before you proceed, note that the 1.0 version of the MicroProfile OpenAPI specification does not define how the /openapi endpoint may be partitioned in the event of multiple JAX-RS applications running on the same server. In other words, you must stick to one JAX-RS application per server instance as the behaviour for handling multiple applications is currently undefined.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-microprofile-openapi.git
cd guide-microprofile-openapi

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project that you will build.

Before you begin, make sure you have all the necessary prerequisites.

Try what you’ll build

The finish directory in the root of this guide contains the finished application. Give it a try before you proceed.

To try out the application, first go to the finish directory and run the following Maven goal to build the application and deploy it to Open Liberty:

cd finish
mvn liberty:run

After you see the following message, your Liberty instance is ready:

The defaultServer server is ready to run a smarter planet.

Next, point your browser to the http://localhost:9080/openapi URL and you’ll see the RESTful APIs of the inventory service. You can also point to the http://localhost:9080/openapi/ui URL for a more interactive view of the deployed APIs. This UI is built from the Open Source Swagger UI and renders the generated /openapi document into a very user friendly page.

After you are finished checking out the application, stop the Liberty instance by pressing CTRL+C in the command-line session where you ran Liberty. Alternatively, you can run the liberty:stop goal from the finish directory in another shell session:

mvn liberty:stop

Generating the OpenAPI document for the inventory service

You can generate an OpenAPI document in various ways. First, because all Jakarta Restful Web Services annotations are processed by default, you can augment your existing Jakarta Restful Web Services annotations with OpenAPI annotations to enrich your APIs with a minimal amount of work. Second, you can use a set of predefined models to manually create all elements of the OpenAPI tree. Finally, you can filter various elements of the OpenAPI tree, changing them to your liking or removing them entirely.

Navigate to the start directory to begin.

When you run Open Liberty in dev mode, dev mode listens for file changes and automatically recompiles and deploys your updates whenever you save a new change. Run the following goal to start Open Liberty in dev mode:

mvn liberty:dev

After you see the following message, your Liberty instance is ready in dev mode:

**************************************************************
*    Liberty is running in dev mode.

Dev mode holds your command-line session to listen for file changes. Open another command-line session to continue, or open the project in your editor.

Because the Jakarta Restful Web Services framework handles basic API generation for Jakarta Restful Web Services annotations, a skeleton OpenAPI tree will be generated from the inventory service. You can use this tree as a starting point and augment it with annotations and code to produce a complete OpenAPI document.

Now, visit the http://localhost:9080/openapi URL to see the generated OpenAPI tree. You can also visit the http://localhost:9080/openapi/ui URL for a more interactive view of the APIs.

Augmenting the existing Jakarta Restful Web Services annotations with OpenAPI annotations

Because all Jakarta Restful Web Services annotations are processed by default, you can augment the existing code with OpenAPI annotations without needing to rewrite portions of the OpenAPI document that are already covered by the Jakarta Restful Web Services framework.

Replace the InventoryResource class.
src/main/java/io/openliberty/guides/inventory/InventoryResource.java

Add OpenAPI @APIResponse, @APIResponseSchema, @Operation, and @Parameter annotations to the two JAX-RS endpoint methods, getPropertiesForHost() and listContents().

InventoryResource.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2017, 2022 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.guides.inventory;
 13
 14import java.util.Properties;
 15import jakarta.enterprise.context.RequestScoped;
 16import jakarta.inject.Inject;
 17import jakarta.ws.rs.GET;
 18import jakarta.ws.rs.Path;
 19import jakarta.ws.rs.PathParam;
 20import jakarta.ws.rs.Produces;
 21import jakarta.ws.rs.core.MediaType;
 22import jakarta.ws.rs.core.Response;
 23
 24import org.eclipse.microprofile.openapi.annotations.Operation;
 25import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
 26import org.eclipse.microprofile.openapi.annotations.media.Content;
 27import org.eclipse.microprofile.openapi.annotations.media.Schema;
 28import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
 29import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
 30import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema;
 31import io.openliberty.guides.inventory.model.InventoryList;
 32
 33@RequestScoped
 34@Path("/systems")
 35public class InventoryResource {
 36
 37    @Inject
 38    InventoryManager manager;
 39
 40    @GET
 41    @Path("/{hostname}")
 42    @Produces(MediaType.APPLICATION_JSON)
 43    // tag::host-property[]
 44    // tag::APIResponse[]
 45    @APIResponse(
 46        responseCode = "404",
 47        description = "Missing description",
 48        content = @Content(mediaType = "application/json"))
 49    // end::APIResponse[]
 50    //tag::APIResponseSchema[]
 51    @APIResponseSchema(value = Properties.class,
 52        responseDescription = "JVM system properties of a particular host.",
 53        responseCode = "200")
 54    // end::APIResponseSchema[]
 55    // tag::Operation[]
 56    @Operation(
 57        summary = "Get JVM system properties for particular host",
 58        description = "Retrieves and returns the JVM system properties from the system "
 59        + "service running on the particular host.")
 60    // end::Operation[]
 61    // end::host-property[]
 62    public Response getPropertiesForHost(
 63        // tag::Parameter[]
 64        @Parameter(
 65            description = "The host for whom to retrieve "
 66                + "the JVM system properties for.",
 67            required = true,
 68            example = "foo",
 69            schema = @Schema(type = SchemaType.STRING))
 70        // end::Parameter[]
 71        @PathParam("hostname") String hostname) {
 72        // Get properties for host
 73        Properties props = manager.get(hostname);
 74        if (props == null) {
 75            return Response.status(Response.Status.NOT_FOUND)
 76                           .entity("{ \"error\" : "
 77                                   + "\"Unknown hostname " + hostname
 78                                   + " or the resource may not be "
 79                                   + "running on the host machine\" }")
 80                           .build();
 81        }
 82
 83        //Add to inventory to host
 84        manager.add(hostname, props);
 85        return Response.ok(props).build();
 86    }
 87
 88    @GET
 89    @Produces(MediaType.APPLICATION_JSON)
 90    // tag::listContents[]
 91    @APIResponseSchema(value = InventoryList.class,
 92        responseDescription = "host:properties pairs stored in the inventory.",
 93        responseCode = "200")
 94    @Operation(
 95        summary = "List inventory contents.",
 96        description = "Returns the currently stored host:properties pairs in the "
 97        + "inventory.")
 98    // end::listContents[]
 99    public InventoryList listContents() {
100        return manager.list();
101    }
102
103}

Clearly, there are many more OpenAPI annotations now, so let’s break them down:

Annotation Description

@APIResponse

Describes a single response from an API operation.

@APIResponseSchema

Convenient short-hand way to specify a simple response with a Java class that could otherwise be specified using @APIResponse.

@Operation

Describes a single API operation on a path.

@Parameter

Describes a single operation parameter.

Because the Open Liberty instance was started in dev mode at the beginning of the guide, your changes were automatically picked up. Refresh the http://localhost:9080/openapi URL to see the updated OpenAPI tree. The two endpoints at which your JAX-RS endpoint methods are served are now more meaningful:

/inventory/systems:
  get:
    summary: List inventory contents.
    description: Returns the currently stored host:properties pairs in the inventory.
    responses:
      "200":
        description: host:properties pairs stored in the inventory.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InventoryList'
/inventory/systems/{hostname}:
  get:
    summary: Get JVM system properties for particular host
    description: Retrieves and returns the JVM system properties from the system
      service running on the particular host.
    parameters:
    - name: hostname
      in: path
      description: The host for whom to retrieve the JVM system properties for.
      required: true
      schema:
        type: string
      example: foo
    responses:
      "404":
        description: Missing description
        content:
          application/json: {}
      "200":
        description: JVM system properties of a particular host.
        content:
          application/json:
            schema:
              type: object

OpenAPI annotations can also be added to POJOs to describe what they represent. Currently, your OpenAPI document doesn’t have a very meaningful description of the InventoryList POJO and hence it’s very difficult to tell exactly what that POJO is used for. To describe the InventoryList POJO in more detail, augment the src/main/java/io/openliberty/guides/inventory/model/InventoryList.java file with some OpenAPI annotations.

Replace the InventoryList class.
src/main/java/io/openliberty/guides/inventory/model/InventoryList.java

Add OpenAPI @Schema annotations to the InventoryList class and the systems variable.

InventoryList.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2017, 2022 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.guides.inventory.model;
13
14import java.util.List;
15import org.eclipse.microprofile.openapi.annotations.media.Schema;
16
17// tag::InventoryList[]
18@Schema(name = "InventoryList",
19description = "POJO that represents the inventory contents.")
20// end::InventoryList[]
21// tag::InventoryListClass[]
22public class InventoryList {
23
24    // tag::Systems[]
25    @Schema(required = true)
26    // end::Systems[]
27    private List<SystemData> systems;
28
29    public InventoryList(List<SystemData> systems) {
30        this.systems = systems;
31    }
32
33    public List<SystemData> getSystems() {
34        return systems;
35    }
36
37    public int getTotal() {
38        return systems.size();
39    }
40}
41// end::InventoryListClass[]

Likewise, annotate the src/main/java/io/openliberty/guides/inventory/model/SystemData.java POJO, which is referenced in the InventoryList class.

Replace the SystemData class.
src/main/java/io/openliberty/guides/inventory/model/SystemData.java

Add OpenAPI @Schema annotations to the SystemData class, the hostname variable and the properties variable.

SystemData.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2017, 2022 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.guides.inventory.model;
13
14import java.util.Properties;
15import org.eclipse.microprofile.openapi.annotations.media.Schema;
16
17// tag::SystemData[]
18@Schema(name = "SystemData",
19       description = "POJO that represents a single inventory entry.")
20// end::SystemData[]
21public class SystemData {
22
23    // tag::Hostname[]
24    @Schema(required = true)
25    // end::Hostname[]
26    private final String hostname;
27
28    // tag::Properties[]
29    @Schema(required = true)
30    // end::Properties[]
31    private final Properties properties;
32
33    public SystemData(String hostname, Properties properties) {
34        this.hostname = hostname;
35        this.properties = properties;
36    }
37
38    public String getHostname() {
39        return hostname;
40    }
41
42    public Properties getProperties() {
43        return properties;
44    }
45
46    @Override
47    public boolean equals(Object host) {
48        if (host instanceof SystemData) {
49            return hostname.equals(((SystemData) host).getHostname());
50        }
51        return false;
52    }
53}

Refresh the http://localhost:9080/openapi URL to see the updated OpenAPI tree:

components:
  schemas:
    InventoryList:
      description: POJO that represents the inventory contents.
      required:
      - systems
      type: object
      properties:
        systems:
          type: array
          items:
            $ref: '#/components/schemas/SystemData'
        total:
          format: int32
          type: integer
    SystemData:
      description: POJO that represents a single inventory entry.
      required:
      - hostname
      - properties
      type: object
      properties:
        hostname:
          type: string
        properties:
          type: object

Filtering the OpenAPI tree elements

Filtering of certain elements and fields of the generated OpenAPI document can be done by using the OASFilter interface.

Create the InventoryOASFilter class.
src/main/java/io/openliberty/guides/inventory/filter/InventoryOASFilter.java

InventoryOASFilter.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2018, 2022 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.guides.inventory.filter;
13
14import java.util.Arrays;
15import java.util.Collections;
16
17import org.eclipse.microprofile.openapi.OASFactory;
18import org.eclipse.microprofile.openapi.OASFilter;
19import org.eclipse.microprofile.openapi.models.OpenAPI;
20import org.eclipse.microprofile.openapi.models.info.License;
21import org.eclipse.microprofile.openapi.models.info.Info;
22import org.eclipse.microprofile.openapi.models.responses.APIResponse;
23import org.eclipse.microprofile.openapi.models.servers.Server;
24import org.eclipse.microprofile.openapi.models.servers.ServerVariable;
25
26public class InventoryOASFilter implements OASFilter {
27
28  @Override
29  // tag::filterAPIResponse[]
30  public APIResponse filterAPIResponse(APIResponse apiResponse) {
31    if ("Missing description".equals(apiResponse.getDescription())) {
32      apiResponse.setDescription("Invalid hostname or the system service may not "
33          + "be running on the particular host.");
34    }
35    return apiResponse;
36  }
37  // end::filterAPIResponse[]
38
39  @Override
40  // tag::filterOpenAPI[]
41  // tag::OpenAPI[]
42  public void filterOpenAPI(OpenAPI openAPI) {
43  // end::OpenAPI[]
44    // tag::oasfactory[]
45    openAPI.setInfo(
46        OASFactory.createObject(Info.class).title("Inventory App").version("1.0")
47                  .description(
48                      "App for storing JVM system properties of various hosts.")
49                  .license(
50                      OASFactory.createObject(License.class)
51                                .name("Eclipse Public License - v 2.0").url(
52                                    "https://www.eclipse.org/legal/epl-2.0")));
53
54    openAPI.addServer(
55        OASFactory.createServer()
56                  .url("http://localhost:{port}")
57                  .description("Simple Open Liberty.")
58                  .variables(Collections.singletonMap("port",
59                                 OASFactory.createServerVariable()
60                                           .defaultValue("9080")
61                                           .description("Server HTTP port."))));
62    // end::oasfactory[]
63  }
64  // end::filterOpenAPI[]
65
66}

The filterAPIResponse() method allows filtering of APIResponse elements. When you override this method, it will be called once for every APIResponse element in the OpenAPI tree. In this case, you are matching the 404 response that is returned by the /inventory/systems/{hostname} endpoint and setting the previously missing description. To remove an APIResponse element or another filterable element, simply return null.

The filterOpenAPI() method allows filtering of the singleton OpenAPI element. Unlike other filter methods, when you override filterOpenAPI(), it is called only once as the last method for a particular filter. Hence, make sure that it doesn’t override any other filter operations that are called before it. Your current OpenAPI document doesn’t provide much information on the application itself or on what server and port it runs on. This information is usually provided in the info and servers elements, which are currently missing. Use the OASFactory class to manually set these and other elements of the OpenAPI tree from the org.eclipse.microprofile.openapi.models package. The OpenAPI element is the only element that cannot be removed, because that would mean removing the whole OpenAPI tree.

Each filtering method is called once for each corresponding element in the model tree. You can think of each method as a callback for various key OpenAPI elements.

Before you can use the filter class that you created, you need to create the microprofile-config.properties file.

Create the configuration file.
src/main/resources/META-INF/microprofile-config.properties

microprofile-config.properties

1# tag::Scan[]
2mp.openapi.scan.disable = true
3# end::Scan[]
4# tag::Config[]
5mp.openapi.filter = io.openliberty.guides.inventory.filter.InventoryOASFilter
6# end::Config[]

This configuration file is picked up automatically by MicroProfile Config and registers your filter by passing in the fully qualified name of the filter class into the mp.openapi.filter property.

Refresh the http://localhost:9080/openapi URL to see the updated OpenAPI tree:

info:
  title: Inventory App
  description: App for storing JVM system properties of various hosts.
  license:
    name: Eclipse Public License - v 2.0
    url: https://www.eclipse.org/legal/epl-2.0
  version: "1.0"
servers:
- url: "http://localhost:{port}"
  description: Simple Open Liberty.
  variables:
    port:
      default: "9080"
      description: Server HTTP port.
responses:
  "404":
    description: Invalid hostname or the system service may not be running on
      the particular host.
    content:
      application/json: {}

For more information about which elements you can filter, see the MicroProfile API documentation.

To learn more about MicroProfile Config, visit the MicroProfile Config GitHub repository and try one of the MicroProfile Config guides.

Using pregenerated OpenAPI documents

As an alternative to generating the OpenAPI model tree from code, you can provide a valid pregenerated OpenAPI document to describe your APIs. This document must be named openapi with a yml, yaml, or json extension and be placed under the META-INF directory. Depending on the scenario, the document might be fully or partially complete. If the document is fully complete, then you can disable annotation scanning entirely by setting the mp.openapi.scan.disable MicroProfile Config property to true. If the document is partially complete, then you can augment it with code.

To use the pre-generated OpenAPI document, create the OpenAPI document YAML file.

Create the OpenAPI document file.
src/main/resources/META-INF/openapi.yaml

openapi.yaml

 1---
 2openapi: 3.0.3
 3info:
 4  title: Inventory App
 5  description: App for storing JVM system properties of various hosts.
 6  license:
 7    name: Eclipse Public License - v 2.0
 8    url: https://www.eclipse.org/legal/epl-2.0
 9  version: "1.0"
10paths:
11  /inventory/properties:
12    get:
13      operationId: getProperties
14      responses:
15        "200":
16          description: JVM system properties of the host running this service.
17          content:
18            application/json:
19              schema:
20                type: object
21                additionalProperties:
22                  type: string
23  /inventory/systems:
24    get:
25      summary: List inventory contents.
26      description: Returns the currently stored host:properties pairs in the inventory.
27      responses:
28        "200":
29          description: host:properties pairs stored in the inventory.
30          content:
31            application/json:
32              schema:
33                $ref: '#/components/schemas/InventoryList'
34  /inventory/systems/{hostname}:
35    get:
36      summary: Get JVM system properties for particular host
37      description: Retrieves and returns the JVM system properties from the system
38        service running on the particular host.
39      parameters:
40      - name: hostname
41        in: path
42        description: The host for whom to retrieve the JVM system properties for.
43        required: true
44        schema:
45          type: string
46        example: foo
47      responses:
48        "404":
49          description: Invalid hostname or the system service may not be running on
50            the particular host.
51          content:
52            application/json: {}
53        "200":
54          description: JVM system properties of a particular host.
55          content:
56            application/json:
57              schema:
58                type: object
59components:
60  schemas:
61    InventoryList:
62      description: POJO that represents the inventory contents.
63      required:
64      - systems
65      type: object
66      properties:
67        systems:
68          type: array
69          items:
70            $ref: '#/components/schemas/SystemData'
71        total:
72          format: int32
73          type: integer
74    SystemData:
75      description: POJO that represents a single inventory entry.
76      required:
77      - hostname
78      - properties
79      type: object
80      properties:
81        hostname:
82          type: string
83        properties:
84          type: object

This document is the same as your current OpenAPI document with extra APIs for the /inventory/properties endpoint. This document is complete so you can also add the mp.openapi.scan.disable property and set it to true in the src/main/resources/META-INF/microprofile-config.properties file.

Replace the configuration file.
src/main/resources/META-INF/microprofile-config.properties

Add and set the mp.openapi.scan.disable property to true.

microprofile-config.properties

1# tag::Scan[]
2mp.openapi.scan.disable = true
3# end::Scan[]
4# tag::Config[]
5mp.openapi.filter = io.openliberty.guides.inventory.filter.InventoryOASFilter
6# end::Config[]

Refresh the http://localhost:9080/openapi URL to see the updated OpenAPI tree:

/inventory/properties:
  get:
    operationId: getProperties
    responses:
      "200":
        description: JVM system properties of the host running this service.
        content:
          application/json:
            schema:
              type: object
              additionalProperties:
                type: string

Testing the service

No automated tests are provided to verify the correctness of the generated OpenAPI document. Manually verify the document by visiting the http://localhost:9080/openapi or the http://localhost:9080/openapi/ui URL.

A few tests are included for you to test the basic functionality of the inventory service. If a test failure occurs, then you might have introduced a bug into the code. These tests will run automatically as a part of the integration test suite.

Running the tests

Because you started Open Liberty in dev mode, you can run the tests by pressing the enter/return key from the command-line session where you started dev mode.

You will see the following output:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.system.SystemEndpointIT
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.4 sec - in it.io.openliberty.guides.system.SystemEndpointIT
Running it.io.openliberty.guides.inventory.InventoryEndpointIT
[WARNING ] Interceptor for {http://client.inventory.guides.openliberty.io/}SystemClient has thrown exception, unwinding now
Could not send Message.
[err] The specified host is unknown: java.net.UnknownHostException: UnknownHostException invoking http://badhostname:9080/inventory/properties: badhostname
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.264 sec - in it.io.openliberty.guides.inventory.InventoryEndpointIT

Results :

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

The warning and error messages are expected and result from a request to a bad or an unknown hostname. This request is made in the testUnknownHost() test from the InventoryEndpointIT integration test.

When you are done checking out the service, exit dev mode by pressing CTRL+C in the command-line session where you ran Liberty.

Great work! You’re done!

You have just documented and filtered the APIs of the inventory service from both the code and a static file by using MicroProfile OpenAPI in Open Liberty.

Feel free to try one of the related MicroProfile guides. They demonstrate additional technologies that you can learn and expand on top of what you built here.

For more in-depth examples of MicroProfile OpenAPI, try one of the demo applications available in the MicroProfile OpenAPI GitHub repository.

Guide Attribution

Documenting RESTful APIs by Open Liberty is licensed under CC BY-ND 4.0

Copy file contents
Copied to clipboard

Prerequisites:

Nice work! Where to next?

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.

Star