Open LibertyのMicroProfile OpenAPIフィーチャーをOpenAPIドキュメント作成ツールとして使う

# What's？

OpenAPIドキュメントを書こうと思った時に、GUIツールを探したりしているのですがなかなか求めるものが見つからないので、ソースコードから生成した方がやりやすいのかなと思うようになりました。

自分はJavaを使うことが多いので、MicroProfileに含まれているMicroProfile OpenAPIとOpen LibertyのMicroProfile OpenAPIフィーチャーを使うのがよいのかなと思いました。

目的がOpenAPIドキュメントの作成に特化する場合は、クラスやメソッドなどの定義だけ書けばよく、中身の実装まではする必要はないですし。

# Open LibertyのMicroProfile OpenAPIフィーチャー

Open LibertyのMicroProfile OpenAPIフィーチャーに関するページはこちらです。

https://openliberty.io/docs/24.0.0.10/reference/feature/mpOpenAPI-3.1.html

このフィーチャーを有効にすると、Jakarta RESTful Web Services（JAX-RS）の`Application`やエンドポイントの内容からOpenAPIドキュメントを生成してくれます。  
また、MicroProfile OpenAPIのアノテーションを使用することで、OpenAPIドキュメントとして必要な情報を付与することもできます。

Swagger UIも付属しているので確認もできますし、Liberty Maven Pluginの`dev`ゴールを使用するとソースコードを編集すればすぐに反映されるので確認も簡単です。

https://github.com/OpenLiberty/ci.maven/blob/liberty-maven-3.11.1/docs/dev.md

こちらを使って、OpenAPIドキュメントを作成したいと思います。

ちなみに、現在のアプリケーションサーバに含まれているMicroProfile OpenAPIのバージョンは3.1で、これはOpenAPI 3.0.xに対応しています。

https://download.eclipse.org/microprofile/microprofile-open-api-3.1.2/microprofile-openapi-spec-3.1.2.html

OpenAPI 3.1.0に対応するのは、次バージョンであるMicroProfile OpenAPI 4.0です。

https://download.eclipse.org/microprofile/microprofile-open-api-4.0.2/microprofile-openapi-spec-4.0.2.html

# 環境

今回の環境はこちらです。

```shell
$ java --version
openjdk 21.0.4 2024-07-16
OpenJDK Runtime Environment (build 21.0.4+7-Ubuntu-1ubuntu222.04)
OpenJDK 64-Bit Server VM (build 21.0.4+7-Ubuntu-1ubuntu222.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: /home/charon/.sdkman/candidates/maven/current
Java version: 21.0.4, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-124-generic", arch: "amd64", family: "unix"
```

# サンプルコード

簡単なサンプルコードを作成。

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>mp-openapi-editor</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>10.0.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile</groupId>
            <artifactId>microprofile</artifactId>
            <version>6.1</version>
            <type>pom</type>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>3.4.0</version>
                </plugin>
                <plugin>
                    <groupId>io.openliberty.tools</groupId>
                    <artifactId>liberty-maven-plugin</artifactId>
                    <version>3.11.1</version>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>io.openliberty.tools</groupId>
                <artifactId>liberty-maven-plugin</artifactId>
                <configuration>
                    <runtimeArtifact>
                        <groupId>io.openliberty</groupId>
                        <artifactId>openliberty-kernel</artifactId>
                        <version>24.0.0.10</version>
                        <type>zip</type>
                    </runtimeArtifact>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```

Open Libertyの設定。少なくとも`restfulWS-3.1`、`mpOpenAPI-3.1`のフィーチャーは必要です。面倒だったらコンビニエンスフィーチャーを指定してもよいでしょう。

```src/main/liberty/config/server.xml
<?xml version="1.0" encoding="UTF-8"?>
<server description="new server">

    <!-- Enable features -->
    <featureManager>
        <feature>restfulWS-3.1</feature>
        <feature>jsonb-3.0</feature>
        <feature>mpOpenAPI-3.1</feature>
    </featureManager>

    <!-- This template enables security. To get the full use of all the capabilities, a keystore and user registry are required. -->

    <!-- For the keystore, default keys are generated and stored in a keystore. To provide the keystore password, generate an
        encoded password using bin/securityUtility encode and add it below in the password attribute of the keyStore element.
        Then uncomment the keyStore element. -->
    <!--
    <keyStore password=""/>
    -->

    <!--For a user registry configuration, configure your user registry. For example, configure a basic user registry using the
        basicRegistry element. Specify your own user name below in the name attribute of the user element. For the password,
        generate an encoded password using bin/securityUtility encode and add it in the password attribute of the user element.
        Then uncomment the user element. -->
    <basicRegistry id="basic" realm="BasicRealm">
        <!--
        <user name="yourUserName" password="" />
        -->
    </basicRegistry>

    <!-- To access this server from a remote client add a host attribute to the following element, e.g. host="*" -->
    <httpEndpoint id="defaultHttpEndpoint"
                  host="0.0.0.0"
                  httpPort="9080"
                  httpsPort="9443" />

    <!-- Automatically expand WAR files and EAR files -->
    <applicationManager autoExpand="true"/>

    <!-- Configures the application on a specified context root -->
    <webApplication contextRoot="/" location="ROOT.war" />

    <!-- Default SSL configuration enables trust for default certificates from the Java runtime -->
    <ssl id="defaultSSLConfig" trustDefaultCerts="true" />
</server>
```

アプリケーションのコンテキストパスは`/`、WARファイル名は`ROOT`にしていて、どのモードで起動してもコンテキストパスが`/`にしています。

ソースコード。

```java:src/main/java/com/example/rest/RestApplication.java
package com.example.rest;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
import org.eclipse.microprofile.openapi.annotations.info.Info;
import org.eclipse.microprofile.openapi.annotations.servers.Server;

@ApplicationPath("/")
@OpenAPIDefinition(
        info = @Info(
                title = "OpenAPI Editor",
                version = "0.0.1"
        ),
        servers = @Server(
                description = "OpenAPI Editor description",
                url = "http://localhost:9080"
        )
)
public class RestApplication extends Application {
}
```

```java:src/main/java/com/example/rest/api/PokemonsResource.java
package com.example.rest.api;

import com.example.rest.model.PokemonRequest;
import com.example.rest.model.PokemonResponse;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

@Path("/pokemons")
@Tag(name = "pokemon")
public class PokemonsResource {
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    @Operation(operationId = "findPokemonById", summary = "ポケモンを取得する", description = "IDを指定してポケモンを取得する")
    @APIResponses({
            @APIResponse(responseCode = "200", description = "指定されたポケモンを取得できた場合"),
            @APIResponse(responseCode = "404", description = "指定されたポケモンが見つからなかった場合")
    })
    public PokemonResponse findById(@PathParam("id") @Parameter(description = "ID") String id) {
        return null;
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Operation(operationId = "createPokemon", summary = "ポケモンを作成する", description = "ポケモンを作成する")
    @APIResponses({
            @APIResponse(responseCode = "201", description = "ポケモンを作成できた場合"),
            @APIResponse(responseCode = "400", description = "リクエストが不正な場合")
    })
    public PokemonResponse create(PokemonRequest pokemon) {
        return null;
    }
}
```

```java:src/main/java/com/example/rest/model/PokemonRequest.java
package com.example.rest.model;

import jakarta.validation.constraints.NotEmpty;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

@Schema(description = "ポケモンリクエスト")
public record PokemonRequest(
        @NotEmpty
        @Schema(description = "名前")
        String name
) {
}
```

```java:src/main/java/com/example/rest/model/PokemonResponse.java
package com.example.rest.model;

import jakarta.validation.constraints.NotEmpty;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

@Schema(description = "ポケモンレスポンス")
public record PokemonResponse(
        @NotEmpty
        @Schema(description = "名前")
        String name
) {
}
```

あとは開発モードで起動しておきます。

```shell
$ mvn compile liberty:dev
```

# 生成されたOpenAPIドキュメントを確認する

生成されたOpenAPIドキュメントの確認。

```shell
$ curl localhost:9080/openapi
```

今回の例だと、こんな感じになりました。

```yaml:---
openapi: 3.0.3
info:
  title: OpenAPI Editor
  version: 0.0.1
servers:
- url: http://localhost:9080
  description: OpenAPI Editor description
tags:
- name: pokemon
paths:
  /pokemons:
    post:
      tags:
      - pokemon
      summary: ポケモンを作成する
      description: ポケモンを作成する
      operationId: createPokemon
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PokemonRequest'
      responses:
        "201":
          description: ポケモンを作成できた場合
        "400":
          description: リクエストが不正な場合
  /pokemons/{id}:
    get:
      tags:
      - pokemon
      summary: ポケモンを取得する
      description: IDを指定してポケモンを取得する
      operationId: findPokemonById
      parameters:
      - name: id
        in: path
        description: ID
        required: true
        schema:
          type: string
      responses:
        "200":
          description: 指定されたポケモンを取得できた場合
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PokemonResponse'
        "404":
          description: 指定されたポケモンが見つからなかった場合
components:
  schemas:
    PokemonRequest:
      description: ポケモンリクエスト
      required:
      - name
      type: object
      properties:
        name:
          description: 名前
          minLength: 1
          type: string
    PokemonResponse:
      description: ポケモンレスポンス
      required:
      - name
      type: object
      properties:
        name:
          description: 名前
          minLength: 1
          type: string
```

Swagger UIで確認したい場合は、`http://localhost:9080/openapi/ui/`にアクセスします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309669/2d4cc5ba-9f7d-2b37-2c8a-f436ceb9e454.png)

Liberty Maven Pluginの開発モードで実行している場合は、ソースコードの変更がすぐに反映されるので便利です。