CLOVER🍀

That was when it all began.

Rustのシリアライズ・デシリアライズフレームワークSerdeでJSONデータを読み書きする

これは、なにをしたくて書いたもの?

Rustでデータ構造を変換するフレームワークとしてSerdeというものがあることに気づいてはいたのですが、ちゃんと
扱ったことがなかったので1度見ておくことにしました。

Serde

SerdeのWebサイトはこちら。

Overview · Serde

GitHubリポジトリーはこちら。

GitHub - serde-rs/serde: Serialization framework for Rust

Serdeは、Rustrのデータ構造を効率的かつ汎用的にシリアライズ・デシリアライズするためのフレームワークです。

Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.

特徴としては、多くの言語がデータのシリアライズにリフレクションを使用することに対して、SerdeはRustの強力な
トレイトシステムをベースに構築されたものだとしています。

Where many other languages rely on runtime reflection for serializing data, Serde is instead built on Rust's powerful trait system.

データ構造とは、Serdeのトレイトを実装しているかderive属性を使用した実装を自動生成したものを指しています。
これによりリフレクションや実行時に型情報を扱うオーバーヘッドを回避できます。

A data structure that knows how to serialize and deserialize itself is one that implements Serde's Serialize and Deserialize traits (or uses Serde's derive attribute to automatically generate implementations at compile time). This avoids any overhead of reflection or runtime type information.

つまり、Serdeはデータ構造によらず手書きのシリアライザーと同等の速度で動作する、ということです。

データフォーマットの実装は、コミュニティにより以下が実装されているようです。

  • JSON
  • Postcard
  • CBOR
  • YAML
  • MessagePack
  • TOML
  • Pickle
  • RON
  • BSON
  • Avro
  • JSON5
  • x-www-form-urlencoded QueryString
  • Starlark
  • Envy
  • Envy Store
  • S式
  • D-Bus
  • FlexBuffers
  • Bencode
  • Token streams
  • DynamoDB Items
  • Hjson
  • CSV

Serdeで扱えるデータ型はこちら。

Serde data model · Serde

derive属性の使い方はこちら。

Using derive · Serde

deriveを使う時のカスタマイズはこちら。

Attributes · Serde

Serdeクレートのドキュメントはこちら。

serde - Rust

ひとまず、SerdeにJSONで慣れていってみましょう。

GitHub - serde-rs/json: Strongly typed JSON library for Rust

Serde JSONクレートのドキュメントはこちらです。

serde_json - Rust

環境

今回の環境はこちら。

$ rustup --version
rustup 1.28.1 (f9edccde0 2025-03-05)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.86.0 (05f9846f8 2025-03-31)`

準備

Cargoパッケージの作成。

$ cargo new --vcs none serde-json-getting-started
$ cd serde-json-getting-started

Serde、Serde JSONクレートを追加。derive属性を使うには、Serdeにderiveフィーチャーが必要になります。
また日時を扱うためにChronoクレートも入れておきます。

$ cargo add serde --features derive
$ cargo add serde_json
$ cargo add chrono --features serde

featureフラグはこちらですね。

serde 1.0.219 - Docs.rs

serde_json 1.0.140 - Docs.rs

Cargo.toml

[package]
name = "serde-json-getting-started"
version = "0.1.0"
edition = "2024"

[dependencies]
chrono = { version = "0.4.40", features = ["serde"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"

Serde JSONをテストコードで試してみる

それでは、Serde JSONを使ってみましょう。

ソースコードの雛形はこんな感じにします。

src/main.rs

use chrono::NaiveDate;
use serde::{Deserialize, Serialize};

fn main() {}

// ここに構造体を書く

#[cfg(test)]
mod tests {
    use chrono::NaiveDate;
    use serde_json::{Value, json};

    use crate::{Book, Point};

    // ここにテストを書く
}

まずはSerdeのトップページに書かれてある構造体を使ってみます。

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

Overview · Serde

derive属性にSerializeDeserializeを追加することで、シリアライズおよびデシリアライズができるようになります。

テストコード。

    #[test]
    fn getting_started_serde_json() {
        let point = Point { x: 1, y: 2 };

        let serialized = serde_json::to_string(&point).unwrap();
        assert_eq!(serialized, r#"{"x":1,"y":2}"#);

        let deserialized: Point = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, Point { x: 1, y: 2 });
    }

それぞれserde_json::to_stringシリアライズserde_json::from_strでデシリアライズを行ったテストコードです。

Crate serde_json / Parsing JSON as strongly typed data structures

またjson!マクロを使うことでJSONデータを組み立てることもできます。この場合、型定義されていない汎用のJSON型に
なります。

    #[test]
    fn using_json_macro() {
        let point: Value = json!({
            "x": 1,
            "y": 2
        });

        assert_eq!(point.get("x").unwrap(), 1);
        assert_eq!(point.get("y").unwrap(), 2);
    }

ChronoクレートのNativeDateをメンバーに含む構造体。

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Book {
    isbn13: String,
    title: String,
    price: i32,
    publish_date: NaiveDate,
}

Chronoクレートの型はそのままではシリアライズ、デシリアライズできませんが、serdeフィーチャーを使うことで
Serdeを使ったシリアライズ、デシリアライズができるようになります。

chrono::serde - Rust

chrono::naive::serde - Rust

テストコード。

    #[test]
    fn book_json() {
        let book = Book {
            isbn13: "978-4873119786".to_string(),
            title: "プログラミングRust 第2版".to_string(),
            price: 5280,
            publish_date: NaiveDate::from_ymd_opt(2022, 1, 19).unwrap(),
        };

        let serialized = serde_json::to_string(&book).unwrap();
        assert_eq!(
            serialized,
            r#"{"isbn13":"978-4873119786","title":"プログラミングRust 第2版","price":5280,"publish_date":"2022-01-19"}"#
        );

        let deserialized: Book = serde_json::from_str(&serialized).unwrap();
        assert_eq!(
            deserialized,
            Book {
                isbn13: "978-4873119786".to_string(),
                title: "プログラミングRust 第2版".to_string(),
                price: 5280,
                publish_date: NaiveDate::from_ymd_opt(2022, 1, 19).unwrap(),
            }
        );
    }

今回はChronoクレートのフィーチャーを使いましたが、Serdeでは自分でChronoのSerializerDeserializerを書く例も
書かれています。

Custom date format · Serde

Vectorを使ったテストコード。

    #[test]
    fn book_vec_json() {
        let books = vec![
            Book {
                isbn13: "978-4873119786".to_string(),
                title: "プログラミングRust 第2版".to_string(),
                price: 5280,
                publish_date: NaiveDate::from_ymd_opt(2022, 1, 19).unwrap(),
            },
            Book {
                isbn13: "978-4065369579".to_string(),
                title: "RustによるWebアプリケーション開発 設計からリリース・運用まで".to_string(),
                price: 4400,
                publish_date: NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
            },
        ];

        let serialized = serde_json::to_string(&books).unwrap();
        assert_eq!(
            serialized,
            r#"[{"isbn13":"978-4873119786","title":"プログラミングRust 第2版","price":5280,"publish_date":"2022-01-19"},{"isbn13":"978-4065369579","title":"RustによるWebアプリケーション開発 設計からリリース・運用まで","price":4400,"publish_date":"2024-09-30"}]"#
        );

        let desealized: Vec<Book> = serde_json::from_str(&serialized).unwrap();
        assert_eq!(
            desealized,
            vec![
                Book {
                    isbn13: "978-4873119786".to_string(),
                    title: "プログラミングRust 第2版".to_string(),
                    price: 5280,
                    publish_date: NaiveDate::from_ymd_opt(2022, 1, 19).unwrap(),
                },
                Book {
                    isbn13: "978-4065369579".to_string(),
                    title: "RustによるWebアプリケーション開発 設計からリリース・運用まで"
                        .to_string(),
                    price: 4400,
                    publish_date: NaiveDate::from_ymd_opt(2024, 9, 30).unwrap(),
                },
            ]
        );
    }

こちらも問題なくシリアライズ、デシリアライズ可能です。

こんなところでしょうか。

おわりに

Rustのシリアライズ・デシリアライズフレームワークであるSerdeを使って、JSONデータを読み書きしてみました。

Serdeはよく見かけるので、どういうものか把握しておけてよかったと思うのと、derive属性を使おうとすると
独自の型についてはSerializerDeserializerの実装が必要になるので、サードパーティー製のクレートを使う場合は
serdeフィーチャーが提供されていないか確認した方がよさそうですね。

WildFly 36 × SmallRye OpenAPI 4.0で出力するOpenAPIドキュメントのバージョンを3.1、3.0に切り替える

これは、なにをしたくて書いたもの?

先日、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>
        <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の拡張プロパティはこちらのソースコードで確認することになります。

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ではexclusiveMinimumbooleanになっていて、別途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が並行して使われると思うので、用途に応じてこういった機能を頼ることになるのかなと
思います。