これは、なにをしたくて書いたもの?
先日、WildFly 36.0.0.Finalがリリースされました。
WildFly 36 is released!
WildFly 35でもMicroProfile 7.0の一部を実装していたのですが、36でMicroProfile 7.0のTCKが通ったようなので少しずつ
見ていこうと思います。
https://github.com/wildfly/certifications/blob/MP7.0/WildFly_36.0.0.Final/microprofile-7.0/microprofile-7.0-full-certification.adoc
今回は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の両方を扱えるようです。
Allow 4.0 to generate OpenAPI v3.0 as well as OpenAPI v3.1 · Issue #1891 · smallrye/smallrye-open-api · GitHub
Support generation of OpenAPI v3.0 by Azquelt · Pull Request #1918 · smallrye/smallrye-open-api · GitHub
使用するプロパティは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>
<mavencompilerrelease>21</mavencompilerrelease>
<projectbuildsourceEncoding>UTF-8</projectbuildsourceEncoding>
<projectreportingoutputEncoding>UTF-8</projectreportingoutputEncoding>
</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の拡張プロパティはこちらのソースコードで確認することになります。
https://github.com/smallrye/smallrye-open-api/blob/4.0.9/core/src/main/java/io/smallrye/openapi/api/SmallRyeOASConfig.java
再度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が並行して使われると思うので、用途に応じてこういった機能を頼ることになるのかなと
思います。