これは、なにをしたくて書いたもの?
OpenAPIでのAPI定義を書こうとした時に、どうするのがいいのかなと思ったりしていたのですが。
Quarkusの場合、ビルド時にOpenAPIの定義ファイルを作成できそうなのでこちらを試してみようかなということで。
Quarkus × SmallRye OpenAPI
QuarkusのOpenAPIに関するドキュメントは、こちら。
Using OpenAPI and Swagger UI - Quarkus
QuarkusではEclipse MicroProfile OpenAPIを使用していて、その実装にはSmallRye OpenAPIを使用しています。
GitHub - eclipse/microprofile-open-api: Microprofile open api
GitHub - smallrye/smallrye-open-api: SmallRye implementation of Eclipse MicroProfile OpenAPI
現時点のQuarkus(2.14.3.Final)が使用しているSmallRye OpenAPIのバージョンは2.3.1で、Eclipse MicroProfile OpenAPIのバージョンは
2.0.1になります。
https://github.com/smallrye/smallrye-open-api/blob/2.3.1/pom.xml#L25
Eclipse MicroProfile OpenAPIの仕様書はこちら。
SmallRye OpenAPIが使用するデフォルトのOpenAPIのバージョンは、3.0.3です。
OpenAPI 3.0.3の仕様書はこちら。
OpenAPI Specification v3.0.3 | Introduction, Definitions, & More
それで、今回の目的のビルド時のOpenAPI定義ファイルの出力ですが、quarkus.smallrye-openapi.store-schema-directory
という
プロパティ(またはQUARKUS_SMALLRYE_OPENAPI_STORE_SCHEMA_DIRECTORY
環境変数)を使用すれば実現できそうです。
このあたりですね。
こちらを試してみましょう。書籍をお題にして、簡単なREST APIを書いてOpenAPI定義ファイルを生成してみたいと思います。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.5 2022-10-18 OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu120.04) OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu120.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-135-generic", arch: "amd64", family: "unix"
Quarkusアプリケーションを作成する
まずは、Quarkusアプリケーションを作成します。SmallRye OpenAPI Extensionを加えつつ、RESTEasy Reactiveに関するExtensionを
加えてプロジェクトを作成。
$ mvn io.quarkus.platform:quarkus-maven-plugin:2.14.3.Final:create \ -DprojectGroupId=org.littlewings \ -DprojectArtifactId=openapi-definition-file-generate \ -DprojectVersion=0.0.1-SNAPSHOT \ -Dextensions='resteasy-reactive,resteasy-reactive-jackson,quarkus-smallrye-openapi' \ -DnoCode
選択されたExtensionなど。
[INFO] ----------- [INFO] selected extensions: - io.quarkus:quarkus-resteasy-reactive - io.quarkus:quarkus-smallrye-openapi - io.quarkus:quarkus-resteasy-reactive-jackson [INFO] applying codestarts... [INFO] 📚 java 🔨 maven 📦 quarkus 📝 config-properties 🔧 dockerfiles 🔧 maven-wrapper [INFO] -----------
プロジェクト内へ移動。
$ cd openapi-definition-file-generate
Maven依存関係など。
<properties> <compiler-plugin.version>3.8.1</compiler-plugin.version> <maven.compiler.release>17</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id> <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id> <quarkus.platform.version>2.14.3.Final</quarkus.platform.version> <skipITs>true</skipITs> <surefire-plugin.version>3.0.0-M7</surefire-plugin.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>${quarkus.platform.group-id}</groupId> <artifactId>${quarkus.platform.artifact-id}</artifactId> <version>${quarkus.platform.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-openapi</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive-jackson</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-arc</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> </dependencies>
ソースコードを書いていきましょう。
お題は書籍にしたので、エンティティ相当のクラスを作っておきます。
src/main/java/org/littlewings/quarkus/openapi/Book.java
package org.littlewings.quarkus.openapi; import java.util.List; public class Book { String isbn; String title; int price; List<String> tags; // getter/setterは省略 }
リクエストとレスポンスにはこのクラスは直接使わず、専用にクラスを作成することにします。
リクエスト用のクラス。
src/main/java/org/littlewings/quarkus/openapi/BookRequest.java
package org.littlewings.quarkus.openapi; import java.util.List; import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "Book Create Request") public class BookRequest { @Schema(required = true, example = "978-4621303252") String isbn; @Schema(required = true, example = "Effective Java 第3版") String title; @Schema(required = true, example = "4400") int price; @Schema(example = "[\"java\", \"programming\"]") List<String> tags; public Book toBook() { Book book = new Book(); book.setIsbn(getIsbn()); book.setTitle(getTitle()); book.setPrice(getPrice()); book.setTags(getTags()); return book; } // getter/setterは省略 }
レスポンス用のクラス。
src/main/java/org/littlewings/quarkus/openapi/BookResponse.java
package org.littlewings.quarkus.openapi; import java.util.List; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(name = "Book Response") public class BookResponse { @Schema(example = "978-4621303252") String isbn; @Schema(example = "Effective Java 第3版") String title; @Schema(example = "4400") int price; @Schema(example = "[\"java\", \"programming\"]") List<String> tags; public static BookResponse fromBook(Book book) { BookResponse response = new BookResponse(); response.setIsbn(book.getIsbn()); response.setTitle(book.getTitle()); response.setPrice(book.getPrice()); response.setTags(book.getTags()); return response; } // getter/setterは省略 }
見るとわかりますが、Eclipse MicroProfile OpenAPIのアノテーションを使ってスキーマ定義や例を記述しています。
MicroProfile OpenAPI Specification / Documentation Mechanisms / Annotations
また、それぞれにエンティティ相当のクラスと変換するメソッドを入れています。
JAX-RSリソースクラス。
src/main/java/org/littlewings/quarkus/openapi/BooksResource.java
package org.littlewings.quarkus.openapi; import java.util.Comparator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.NotFoundException; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.reactive.RestPath; import org.jboss.resteasy.reactive.RestResponse; @Path("books") @Tag(name = "book", description = "Book operations") public class BooksResource { ConcurrentMap<String, Book> store = new ConcurrentHashMap<>(); @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get book by isbn", operationId = "getBook") public Uni<BookResponse> get(@RestPath String isbn) { return Uni .createFrom() .item(store.get(isbn)) .onItem() .ifNull() .failWith(() -> new NotFoundException(String.format("Not found: %s", isbn))) .onItem() .transform(BookResponse::fromBook); } @GET @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "List books", operationId = "listBooks") public Multi<BookResponse> list() { System.out.println(Thread.currentThread().getName()); return Multi .createFrom() .items(store.values().stream().sorted(Comparator.comparing(Book::getPrice).reversed())) .onItem() .transform(BookResponse::fromBook); } @POST @Consumes(MediaType.APPLICATION_JSON) @Operation(summary = "Create book", operationId = "createBook") public Uni<RestResponse<Void>> create(@Context UriInfo uriInfo, BookRequest bookRequest) { store.put(bookRequest.getIsbn(), bookRequest.toBook()); return Uni .createFrom() .item(RestResponse.created(UriBuilder.fromPath(uriInfo.getPath()).path(bookRequest.getIsbn()).build())); } @DELETE @Path("{isbn}") @Operation(summary = "Delete book by isbn", operationId = "deleteBook") public Uni<RestResponse<Void>> delete(@RestPath String isbn) { store.remove(isbn); return Uni.createFrom().item(RestResponse.noContent()); } }
こちらもEclipse MicroProfile OpenAPIのアノテーションを使用しています。
@Operation
の情報はある程度Quarkusでも自動生成できそうですが、今回は明示的に付与することにしました。
Using OpenAPI and Swagger UI / Auto-generation of Operation Id
application.properties
では、アプリケーションレベルのプロパティ設定を少ししておきました。
src/main/resources/application.properties
quarkus.smallrye-openapi.info-title=OpenAPI Definition Example quarkus.smallrye-openapi.info-version=0.0.1
これでアプリケーションとしての準備は完了です。
開発モードで動作確認してみましょう。
$ mvn compile quarkus:dev
データの登録。
$ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books -d '{"isbn": "978-4621303252", "title": "Effective Java 第3版", "price": 4400, "tags": ["java", "programming"]}' HTTP/1.1 201 Created Location: http://localhost:8080/books/978-4621303252 content-length: 0 $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books -d '{"isbn": "978-1782169970", "title": "Infinispan Data Grid Platform Definitive Guide", "price": 5242, "tags": ["in-memory-data-grid"]}' HTTP/1.1 201 Created Location: http://localhost:8080/books/978-1782169970 content-length: 0 $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books -d '{"isbn": "978-4798161488", "title": "MySQL徹底入門 第4版 MySQL 8.0対応", "price": 4180, "tags": ["mysql", "database"]}' HTTP/1.1 201 Created Location: http://localhost:8080/books/978-4798161488 content-length: 0 $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/books -d '{"isbn": "978-1098116743", "title": "Terraform: Up & Running; Writing Infrastructure As Code", "price": 7404, "tags": ["terraform", "infra-structure-as-code"]}' HTTP/1.1 201 Created Location: http://localhost:8080/books/978-1098116743 content-length: 0
複数件のデータの取得。
$ curl -i localhost:8080/books HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 transfer-encoding: chunked [{"isbn":"978-1098116743","title":"Terraform: Up & Running; Writing Infrastructure As Code","price":7404,"tags":["terraform","infra-structure-as-code"]},{"isbn":"978-1782169970","title":"Infinispan Data Grid Platform Definitive Guide","price":5242,"tags":["in-memory-data-grid"]},{"isbn":"978-4621303252","title":"Effective Java 第3版","price":4400,"tags":["java","programming"]},{"isbn":"978-4798161488","title":"MySQL徹底入門 第4版 MySQL 8.0対応","price":4180,"tags":["mysql","database"]}] $ curl -s localhost:8080/books | jq [ { "isbn": "978-1098116743", "title": "Terraform: Up & Running; Writing Infrastructure As Code", "price": 7404, "tags": [ "terraform", "infra-structure-as-code" ] }, { "isbn": "978-1782169970", "title": "Infinispan Data Grid Platform Definitive Guide", "price": 5242, "tags": [ "in-memory-data-grid" ] }, { "isbn": "978-4621303252", "title": "Effective Java 第3版", "price": 4400, "tags": [ "java", "programming" ] }, { "isbn": "978-4798161488", "title": "MySQL徹底入門 第4版 MySQL 8.0対応", "price": 4180, "tags": [ "mysql", "database" ] } ]
単一データの取得。
$ curl -i localhost:8080/books/978-4798161488 HTTP/1.1 200 OK content-length: 118 Content-Type: application/json;charset=UTF-8 {"isbn":"978-4798161488","title":"MySQL徹底入門 第4版 MySQL 8.0対応","price":4180,"tags":["mysql","database"]}
データの削除。
$ curl -i -XDELETE localhost:8080/books/978-4798161488 HTTP/1.1 204 No Content
削除確認。
$ curl -i localhost:8080/books/978-4798161488 HTTP/1.1 404 Not Found Content-Type: application/json content-length: 0
OKですね。
Quarkusが生成するOpenAPIとSwagger UIも見ておきましょう。
$ curl localhost:8080/q/openapi --- openapi: 3.0.3 info: title: OpenAPI Definition Example version: 0.0.1 tags: - name: book description: Book operations paths: /books: get: tags: - book summary: List books operationId: listBooks responses: "200": description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Book Response' post: tags: - book summary: Create book operationId: createBook requestBody: content: application/json: schema: $ref: '#/components/schemas/Book Create Request' responses: "200": description: OK content: application/json: schema: type: object /books/{isbn}: get: tags: - book summary: Get book by isbn operationId: getBook parameters: - name: isbn in: path required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Book Response' delete: tags: - book summary: Delete book by isbn operationId: deleteBook parameters: - name: isbn in: path required: true schema: type: string responses: "200": description: OK content: application/json: schema: type: object components: schemas: Book Create Request: required: - isbn - title - price type: object properties: isbn: type: string example: 978-4621303252 title: type: string example: Effective Java 第3版 price: format: int32 type: integer example: 4400 tags: type: array items: type: string example: - java - programming Book Response: type: object properties: isbn: type: string example: 978-4621303252 title: type: string example: Effective Java 第3版 price: format: int32 type: integer example: 4400 tags: type: array items: type: string example: - java - programming
Swagger UIは、http://localhost:8080/q/swagger-ui/
にアクセスして確認します。
こちらもOKですね。
ビルド時にOpenAPIの定義ファイルを生成する
では、quarkus.smallrye-openapi.store-schema-directory
プロパティを使ってみます。
ここで、quarkus.smallrye-openapi.store-schema-directory
プロパティの説明を見てみます。
If set, the generated OpenAPI schema documents will be stored here on build. Both openapi.json and openapi.yaml will be stored here if this is set.
このプロパティを設定すると、指定したディレクトリにopenapi.json
とopenapi.yaml
の2つのファイルをビルド時に作成するようです。
まずはapplication.properties
にquarkus.smallrye-openapi.store-schema-directory
プロパティを追加してみます。
src/main/resources/application.properties
quarkus.smallrye-openapi.info-title=OpenAPI Definition Example quarkus.smallrye-openapi.info-version=0.0.1 quarkus.smallrye-openapi.store-schema-directory=openapi-definition
openapi-definition
ディレクトリに出力するようにしてみましょう。
ビルド。
$ mvn compile
これでは出力されないようです。
$ ll openapi-definition ls: 'openapi-definition' にアクセスできません: そのようなファイルやディレクトリはありません
パッケージングしてみます。
$ mvn package
すると、こんなログが現れました。
[INFO] [io.quarkus.smallrye.openapi] OpenAPI JSON saved: /path/to/openapi-definition/openapi.json [INFO] [io.quarkus.smallrye.openapi] OpenAPI YAML saved: /path/to/openapi-definition/openapi.yaml
今度は生成されたようです。
$ ll openapi-definition 合計 20 drwxrwxr-x 2 xxxxx xxxxx 4096 12月 16 00:08 ./ drwxrwxr-x 7 xxxxx xxxxx 4096 12月 16 00:08 ../ -rw-rw-r-- 1 xxxxx xxxxx 4190 12月 16 00:08 openapi.json -rw-rw-r-- 1 xxxxx xxxxx 2749 12月 16 00:08 openapi.yaml
確認。
openapi-definition/openapi.yaml
--- openapi: 3.0.3 info: title: OpenAPI Definition Example version: 0.0.1 servers: - url: http://localhost:8080 description: Auto generated value - url: http://0.0.0.0:8080 description: Auto generated value tags: - name: book description: Book operations paths: /books: get: tags: - book summary: List books operationId: listBooks responses: "200": description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Book Response' post: tags: - book summary: Create book operationId: createBook requestBody: content: application/json: schema: $ref: '#/components/schemas/Book Create Request' responses: "200": description: OK content: application/json: schema: type: object /books/{isbn}: get: tags: - book summary: Get book by isbn operationId: getBook parameters: - name: isbn in: path required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Book Response' delete: tags: - book summary: Delete book by isbn operationId: deleteBook parameters: - name: isbn in: path required: true schema: type: string responses: "200": description: OK content: application/json: schema: type: object components: schemas: Book Create Request: required: - isbn - title - price type: object properties: isbn: type: string example: 978-4621303252 title: type: string example: Effective Java 第3版 price: format: int32 type: integer example: 4400 tags: type: array items: type: string example: - java - programming Book Response: type: object properties: isbn: type: string example: 978-4621303252 title: type: string example: Effective Java 第3版 price: format: int32 type: integer example: 4400 tags: type: array items: type: string example: - java - programming
openapi-definition/openapi.json
{ "openapi" : "3.0.3", "info" : { "title" : "OpenAPI Definition Example", "version" : "0.0.1" }, "servers" : [ { "url" : "http://localhost:8080", "description" : "Auto generated value" }, { "url" : "http://0.0.0.0:8080", "description" : "Auto generated value" } ], "tags" : [ { "name" : "book", "description" : "Book operations" } ], "paths" : { "/books" : { "get" : { "tags" : [ "book" ], "summary" : "List books", "operationId" : "listBooks", "responses" : { "200" : { "description" : "OK", "content" : { "application/json" : { "schema" : { "type" : "array", "items" : { "$ref" : "#/components/schemas/Book Response" } } } } } } }, "post" : { "tags" : [ "book" ], "summary" : "Create book", "operationId" : "createBook", "requestBody" : { "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/Book Create Request" } } } }, "responses" : { "200" : { "description" : "OK", "content" : { "application/json" : { "schema" : { "type" : "object" } } } } } } }, "/books/{isbn}" : { "get" : { "tags" : [ "book" ], "summary" : "Get book by isbn", "operationId" : "getBook", "parameters" : [ { "name" : "isbn", "in" : "path", "required" : true, "schema" : { "type" : "string" } } ], "responses" : { "200" : { "description" : "OK", "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/Book Response" } } } } } }, "delete" : { "tags" : [ "book" ], "summary" : "Delete book by isbn", "operationId" : "deleteBook", "parameters" : [ { "name" : "isbn", "in" : "path", "required" : true, "schema" : { "type" : "string" } } ], "responses" : { "200" : { "description" : "OK", "content" : { "application/json" : { "schema" : { "type" : "object" } } } } } } } }, "components" : { "schemas" : { "Book Create Request" : { "required" : [ "isbn", "title", "price" ], "type" : "object", "properties" : { "isbn" : { "type" : "string", "example" : "978-4621303252" }, "title" : { "type" : "string", "example" : "Effective Java 第3版" }, "price" : { "format" : "int32", "type" : "integer", "example" : 4400 }, "tags" : { "type" : "array", "items" : { "type" : "string" }, "example" : [ "java", "programming" ] } } }, "Book Response" : { "type" : "object", "properties" : { "isbn" : { "type" : "string", "example" : "978-4621303252" }, "title" : { "type" : "string", "example" : "Effective Java 第3版" }, "price" : { "format" : "int32", "type" : "integer", "example" : 4400 }, "tags" : { "type" : "array", "items" : { "type" : "string" }, "example" : [ "java", "programming" ] } } } } } }
出力されていますね。
これで、まずは目的は達成できました。
とはいえ、ビルド時にしか使わないのにapplication.properties
に書いておくのはちょっと抵抗があったので、いったんコメントアウト。
src/main/resources/application.properties quarkus.smallrye-openapi.info-title=OpenAPI Definition Example quarkus.smallrye-openapi.info-version=0.0.1 #quarkus.smallrye-openapi.store-schema-directory=openapi-definition
これでもOKです。
$ mvn package -Dquarkus.smallrye-openapi.store-schema-directory=openapi-definition
なんなら、quarkus:dev
でも有効です。
$ mvn compile quarkus:dev -Dquarkus.smallrye-openapi.store-schema-directory=openapi-definition
quarkus:dev
の場合、最初の起動時に1度OpenAPIの定義ファイルが出力され、その後はアプリケーションの再ロードの度にファイルが更新されて
いきます。
あとは、環境変数でも試してみましょう。
$ QUARKUS_SMALLRYE_OPENAPI_STORE_SCHEMA_DIRECTORY=openapi-definition mvn package
こちらもOKでした。
これで、今回確認したいことはできましたね。
まとめ
QuarkusとSmallRye OpenAPIで、ビルド時にOpenAPIの定義ファイルを出力してみました。
ドキュメントのプロパティを眺めていて、求めていたものがあったので使ってみたら、割とあっさり使えたので良かったですね。
ちなみに、SmallRye OpenAPIのMavenプラグイン(またはGradleプラグイン)を使っても同じようなことができそうなので、そのうち
試してみてもいいかなと思いました。