これは、なにをしたくて書いたもの?
先日、WildFly 36.0.0.Finalがリリースされました。
WildFly 35でもMicroProfile 7.0の一部を実装していたのですが、36でMicroProfile 7.0のTCKが通ったようなので少しずつ
見ていこうと思います。
今回はMicroProfile OpenAPIを扱います。
MicroProfile OpenAPI 4.0
MicroProfile 7.0に含まれるMicroProfile OpenAPI仕様のバージョンは、4.0です。
現時点では、正確には4.0.2ですね。
MicroProfile OpenAPI Specification
MicroProfile OpenAPI 4.0と、その前のMicroProfile OpenAPI 3.1での変更点はこちらです。
Release Notes / Release Notes for MicroProfile OpenAPI 4.0
大きな変更点は、扱うOpenAPIのバージョンが3.0から3.1になったことではないでしょうか。
/openapi endpoint now serves documentation in OpenAPI v3.1 format
MicroProfile OpenAPI 4.0には、これに伴う変更が多数含まれています。また、仕様書を見る限りはMicroProfile OpenAPI 4.0が
OpenAPI 3.0に対応している様子はありません。
OpenAPI 3.0と3.1には、互換性のない変更があります。OpenAPI 3.0と3.1の変更点のサマリーはこちらです。
Migrating from OpenAPI 3.0 to 3.1.0 - OpenAPI Initiative
SmallRye OpenAPI 4.0
WildFlyのMicroProfile OpenAPIの実装は、SmallRye OpenAPIです。SmallRye OpenAPI 4.0で、MicroProfile OpenAPI 4.0に
対応しています。
前述のとおりOpenAPI 3.1と3.0には互換性がなく、3.0.3もよく使われるバージョンなのでOpenAPI 3.1のみだとちょっと
困るのかなと思っていたのですが、どうやらSmallRye OpenAPI 4.0ではOpenAPI 3.1と3.0の両方を扱えるようです。
使用するプロパティはmp.openapi.extensions.smallrye.openapiです。
今回、こちらを試してみることにしました。
環境
今回の環境はこちら。
$ java --version openjdk 21.0.6 2025-01-21 OpenJDK Runtime Environment (build 21.0.6+7-Ubuntu-124.04.1) OpenJDK 64-Bit Server VM (build 21.0.6+7-Ubuntu-124.04.1, mixed mode, sharing) $ mvn --version Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.6, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "6.8.0-57-generic", arch: "amd64", family: "unix"
サンプルアプリケーションを作成する
まずはJakarta RESTful Web Services(JAX-RS)とMicroProfile OpenAPIを使ったサンプルアプリケーションを作成します。
Maven依存関係などはこちら。
<packaging>war</packaging> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.wildfly.bom</groupId> <artifactId>wildfly-ee-with-tools</artifactId> <version>36.0.0.Final</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.wildfly.bom</groupId> <artifactId>wildfly-expansion</artifactId> <version>36.0.0.Final</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>jakarta.ws.rs</groupId> <artifactId>jakarta.ws.rs-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>jakarta.json.bind</groupId> <artifactId>jakarta.json.bind-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.eclipse.microprofile.openapi</groupId> <artifactId>microprofile-openapi-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>io.smallrye</groupId> <artifactId>smallrye-open-api-ui</artifactId> <version>4.0.9</version> <scope>runtime</scope> </dependency> </dependencies> <build> <finalName>ROOT</finalName> <plugins> <plugin> <groupId>org.wildfly.plugins</groupId> <artifactId>wildfly-maven-plugin</artifactId> <version>5.1.2.Final</version> <executions> <execution> <id>package</id> <goals> <goal>package</goal> </goals> </execution> </executions> <configuration> <overwrite-provisioned-server>true</overwrite-provisioned-server> <discover-provisioning-info> <version>36.0.0.Final</version> </discover-provisioning-info> </configuration> </plugin> </plugins> </build>
せっかくなので、SmallRye OpenAPIが提供するSwagger UIもつけています。
<dependency> <groupId>io.smallrye</groupId> <artifactId>smallrye-open-api-ui</artifactId> <version>4.0.9</version> <scope>runtime</scope> </dependency>
JAX-RSの有効化。
src/main/java/org/littlewings/wildfly/openapi/RestApplication.java
package org.littlewings.wildfly.openapi; 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; @OpenAPIDefinition( info = @Info( title = "My Sample REST API", version = "0.0.1" ), servers = @Server( description = "My Sample REST API Server description", url = "http://localhost:8080" ) ) @ApplicationPath("/api") public class RestApplication extends Application { }
JAX-RSリソースクラス。
src/main/java/org/littlewings/wildfly/openapi/BooksResource.java
package org.littlewings.wildfly.openapi; import jakarta.validation.Valid; import jakarta.validation.constraints.Size; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import java.util.Comparator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; @Path("/books") @Tag(name = "book") public class BooksResource { private ConcurrentMap<String, BookResponse> store = new ConcurrentHashMap<>(); @PUT @Path("/{isbn13}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "registerBook", summary = "指定されたISBNに書籍を登録する", description = "指定されたISBNに対応する書籍を登録する") @APIResponse(responseCode = "200", description = "書籍が登録できたことを表す") @APIResponse(responseCode = "400", description = "バリデーションでNGになったことを表す") public BookResponse register(@Size(min = 14, max = 14) @PathParam("isbn13") @Parameter(description = "ISBN", example = "123-4567890123") String isbn13, @Valid @RequestBody(required = true, description = "登録する書籍データ") BookRequest bookRequest) { return store.compute(isbn13, (i, before) -> new BookResponse( i, bookRequest.title(), bookRequest.price(), bookRequest.publishDate() ) ); } @GET @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "findAllBooks", summary = "登録された書籍をすべて返却する", description = "登録された書籍を価格の降順にソートしてすべて返却する") @APIResponse(responseCode = "200", description = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す") public List<BookResponse> findAll() { return store.values().stream().sorted(Comparator.comparing(BookResponse::price).reversed()).toList(); } @GET @Path("/{isbn13}") @Produces(MediaType.APPLICATION_JSON) @Operation(operationId = "findBookByIsbn13", summary = "指定されたISBNに対応する書籍を取得する", description = "指定されたISBNに対応する書籍を取得する") @APIResponse(responseCode = "200", description = "指定された書籍が取得できたことを表す") @APIResponse(responseCode = "404", description = "指定された書籍が存在しなかったことを表す") public BookResponse findByIsbn13(@Size(min = 14, max = 14) @PathParam("isbn13") @Parameter(description = "ISBN", example = "123-4567890123") String isbn13) { return store.get(isbn13); } @DELETE @Path("/{isbn13}") @Operation(operationId = "deleteBookByIsbn13", summary = "指定されたISBNに対応する書籍を削除する", description = "指定されたISBNに対応する書籍を削除する") @APIResponse(responseCode = "204", description = "書籍が削除されていることを表す") public void delete(@Size(min = 14, max = 14) @PathParam("isbn13") @Parameter(description = "ISBN", example = "123-4567890123") String isbn13) { store.remove(isbn13); } }
リクエストを表すモデル。
src/main/java/org/littlewings/wildfly/openapi/BookRequest.java
package org.littlewings.wildfly.openapi; import jakarta.json.bind.annotation.JsonbDateFormat; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Size; import java.time.LocalDate; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(description = "登録する書籍に関するリクエスト") public record BookRequest( @NotEmpty @Size(max = 100) @Schema(description = "登録する書籍のタイトル", examples = "Javaの本") String title, @NotNull @Positive @Schema(description = "登録する書籍の価格", examples = "1500") Integer price, @NotNull @JsonbDateFormat("uuuu-MM-dd") @Schema(description = "登録する書籍の出版日", examples = "2024-10-13") LocalDate publishDate ) { }
レスポンスを表すモデル。
src/main/java/org/littlewings/wildfly/openapi/BookResponse.java
package org.littlewings.wildfly.openapi; import jakarta.json.bind.annotation.JsonbDateFormat; import java.time.LocalDate; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(description = "登録されている書籍") public record BookResponse( @Schema(description = "ISBN", examples = "123-4567890123") String isbn13, @Schema(description = "書籍のタイトル", examples = "Javaの本") String title, @Schema(description = "書籍の価格", examples = "1500") Integer price, @JsonbDateFormat("uuuu-MM-dd") @Schema(description = "書籍の出版日", examples = "2024-10-13") LocalDate publishDate ) { }
これでサンプルアプリケーションの準備は完了です。
OpenAPI 3.1.0のOpenAPIドキュメントを生成する
まずはOpenAPI 3.1.0のOpenAPIドキュメントを生成してみましょう。
WildFlyを起動。
$ mvn wildfly:run
SmallRye OpenAPIのUIを含めているので、/openapi-uiでSwagger UIを確認できます。

YAMLで見てみましょう。
$ curl localhost:8080/openapi
返ってきたOpenAPIドキュメントはこちら。OpenAPI 3.1.0ですね。
--- openapi: 3.1.0 components: schemas: BookRequest: description: 登録する書籍に関するリクエスト type: object required: - title - price - publishDate properties: title: type: string description: 登録する書籍のタイトル examples: - Javaの本 maxLength: 100 minLength: 1 price: type: integer format: int32 description: 登録する書籍の価格 examples: - 1500 exclusiveMinimum: 0 publishDate: type: string format: date examples: - 2024-10-13 description: 登録する書籍の出版日 BookResponse: description: 登録されている書籍 type: object properties: isbn13: type: string description: ISBN examples: - 123-4567890123 title: type: string description: 書籍のタイトル examples: - Javaの本 price: type: integer format: int32 description: 書籍の価格 examples: - 1500 publishDate: type: string format: date examples: - 2024-10-13 description: 書籍の出版日 info: title: My Sample REST API version: 0.0.1 servers: - url: http://localhost:8080 description: My Sample REST API Server description tags: - name: book paths: /api/books: get: summary: 登録された書籍をすべて返却する description: 登録された書籍を価格の降順にソートしてすべて返却する operationId: findAllBooks tags: - book responses: "200": description: 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す content: application/json: schema: type: array items: $ref: "#/components/schemas/BookResponse" /api/books/{isbn13}: put: summary: 指定されたISBNに書籍を登録する description: 指定されたISBNに対応する書籍を登録する operationId: registerBook tags: - book parameters: - description: ISBN example: 123-4567890123 name: isbn13 in: path required: true schema: type: string minLength: 14 maxLength: 14 requestBody: description: 登録する書籍データ required: true content: application/json: schema: $ref: "#/components/schemas/BookRequest" responses: "200": description: 書籍が登録できたことを表す content: application/json: schema: $ref: "#/components/schemas/BookResponse" "400": description: バリデーションでNGになったことを表す get: summary: 指定されたISBNに対応する書籍を取得する description: 指定されたISBNに対応する書籍を取得する operationId: findBookByIsbn13 tags: - book parameters: - description: ISBN example: 123-4567890123 name: isbn13 in: path required: true schema: type: string minLength: 14 maxLength: 14 responses: "200": description: 指定された書籍が取得できたことを表す content: application/json: schema: $ref: "#/components/schemas/BookResponse" "404": description: 指定された書籍が存在しなかったことを表す delete: summary: 指定されたISBNに対応する書籍を削除する description: 指定されたISBNに対応する書籍を削除する operationId: deleteBookByIsbn13 tags: - book parameters: - description: ISBN example: 123-4567890123 name: isbn13 in: path required: true schema: type: string minLength: 14 maxLength: 14 responses: "204": description: 書籍が削除されていることを表す
OpenAPI 3.0.3のOpenAPIドキュメントを生成する
では、OpenAPI 3.0.3のOpenAPIドキュメントを生成してみます。
これを行うには、MicroProfile OpenAPIの実装であるSmallRye OpenAPIの拡張プロパティを使う必要があります。
smallryeがSmallRye OpenAPIのvendor prefixになります。こんな感じで指定。
src/main/resources/META-INF/microprofile-config.properties
mp.openapi.extensions.smallrye.openapi = 3.0.3
SmallRye OpenAPIの拡張プロパティはこちらのソースコードで確認することになります。
再度WildFlyを起動。
$ mvn wildfly:run
Swagger UIで確認すると、OASの表示が3.0になっています。

YAMLで確認してみましょう。
--- openapi: 3.0.3 components: schemas: BookRequest: description: 登録する書籍に関するリクエスト required: - title - price - publishDate type: object properties: title: description: 登録する書籍のタイトル maxLength: 100 minLength: 1 type: string example: Javaの本 price: format: int32 description: 登録する書籍の価格 minimum: 0 exclusiveMinimum: true type: integer example: 1500 publishDate: format: date description: 登録する書籍の出版日 type: string example: 2024-10-13 BookResponse: description: 登録されている書籍 type: object properties: isbn13: description: ISBN type: string example: 123-4567890123 title: description: 書籍のタイトル type: string example: Javaの本 price: format: int32 description: 書籍の価格 type: integer example: 1500 publishDate: format: date description: 書籍の出版日 type: string example: 2024-10-13 info: title: My Sample REST API version: 0.0.1 servers: - url: http://localhost:8080 description: My Sample REST API Server description tags: - name: book paths: /api/books: get: summary: 登録された書籍をすべて返却する description: 登録された書籍を価格の降順にソートしてすべて返却する operationId: findAllBooks tags: - book responses: "200": description: 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す content: application/json: schema: type: array items: $ref: "#/components/schemas/BookResponse" /api/books/{isbn13}: put: summary: 指定されたISBNに書籍を登録する description: 指定されたISBNに対応する書籍を登録する operationId: registerBook tags: - book parameters: - description: ISBN example: 123-4567890123 name: isbn13 in: path required: true schema: maxLength: 14 minLength: 14 type: string requestBody: description: 登録する書籍データ required: true content: application/json: schema: $ref: "#/components/schemas/BookRequest" responses: "200": description: 書籍が登録できたことを表す content: application/json: schema: $ref: "#/components/schemas/BookResponse" "400": description: バリデーションでNGになったことを表す get: summary: 指定されたISBNに対応する書籍を取得する description: 指定されたISBNに対応する書籍を取得する operationId: findBookByIsbn13 tags: - book parameters: - description: ISBN example: 123-4567890123 name: isbn13 in: path required: true schema: maxLength: 14 minLength: 14 type: string responses: "200": description: 指定された書籍が取得できたことを表す content: application/json: schema: $ref: "#/components/schemas/BookResponse" "404": description: 指定された書籍が存在しなかったことを表す delete: summary: 指定されたISBNに対応する書籍を削除する description: 指定されたISBNに対応する書籍を削除する operationId: deleteBookByIsbn13 tags: - book parameters: - description: ISBN example: 123-4567890123 name: isbn13 in: path required: true schema: maxLength: 14 minLength: 14 type: string responses: "204": description: 書籍が削除されていることを表す
OpenAPI 3.0.3になっていますね。
openapi: 3.0.3
OpenAPI 3.1と3.0での変更点を確認する
これだけ見ると、バージョン表記が変わっただけでは?とも見えるので、OpenAPI 3.1と3.0で変わったところも見てみましょう。
ちょうど変更点にあたるものを含めているので。
OpenAPI 3.1ではexclusiveMinimumというキーワードは、指定した値の下限を含まないという意味になります。
こちらがOpenAPI 3.1.0で生成したOpenAPIドキュメントの抜粋です。
price: type: integer format: int32 description: 登録する書籍の価格 examples: - 1500 exclusiveMinimum: 0
OpenAPI 3.0ではexclusiveMinimumはbooleanになっていて、別途minimumの指定が必要です。OpenAPI 3.0.3で生成した
OpenAPIドキュメントの抜粋はこちら。
price: format: int32 description: 登録する書籍の価格 minimum: 0 exclusiveMinimum: true type: integer example: 1500
ちゃんとバージョンに合わせて出力内容が変わっていることが確認できました。
おわりに
リリースされたばかりのWildFly 36を使って、SmallRye OpenAPIが出力するOpenAPIドキュメントのバージョンを切り替えて
みました。
MicroProfile OpenAPI 4.0がOpenAPI 3.1をターゲットにしていたのは仕様書からわかっていたので、OpenAPI 3.0.3を
使いたい時にはどうするんだろう?と思っていたところに、この拡張を前々から見つけていたので今回試してみました。
まだしばらくはOpenAPI 3.1と3.0が並行して使われると思うので、用途に応じてこういった機能を頼ることになるのかなと
思います。