CLOVER🍀

That was when it all began.

OpenAPI GeneratorでJAX-RS(RESTEasy)のサーバーサイドのソースコードを生成してみる

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

こちらのエントリーで、MicroProfile OpenAPIを使ってOpenAPIドキュメントを生成してみました。

WildFly 33とMicroProfile OpenAPI(SmallRye OpenAPI)でOpenAPIドキュメントを生成する - CLOVER🍀

今回はこのOpenAPIドキュメントから、Jakarta RESTful Web Services(JAX-RS)のサーバーサイドのソースコードを生成してみたいと思います。

OpenAPI GeneratorでJAX-RSソースコードを生成する

OpenAPIドキュメントから、ソースコードを生成するツールといえばOpenAPI Generatorがよく使われていると思います。

Hello from OpenAPI Generator | OpenAPI Generator

現在のバージョンは7.9.0で、現時点でリストアップされているGeneratorを見てみます。

Generators List | OpenAPI Generator

JAX-RSの名前が入っているものは、これだけあります。

JAX-RSの実装を決めたところで、どうすれば…?という気分になります…。

これだけ種類が多いのは、OpenAPI GeneratorのもとになったSwagger Codegenの頃からのようです。OpenAPI Generatorで追加された
JAX-RS向けのGeneratorもあるようですが。

GitHub - swagger-api/swagger-codegen: swagger-codegen contains a template-driven engine to generate documentation, API clients and server stubs in different languages by parsing your OpenAPI / Swagger definition.

今回はWildFlyを使おうと思うので実装はRESTEasyになるのですが、それでもjaxrs-spec、jaxrs-resteasy、jaxrs-resteasy-eapの3択になります。

それぞれのhelpTextを見ても、なにが違うのかがよくわかりません…。

  • jaxrs-spec … Generates a Java JAXRS Server according to JAXRS 2.0 specification.
  • jaxrs-resteasy … Generates a Java JAXRS-Resteasy Server application.
  • jaxrs-resteasy-eap … Generates a Java JAXRS-Resteasy Server application.

jaxrs-specにlibraryがあるところを踏まえると、どのGeneratorもjaxrs-specのlibraryも一種にしてもよかったのではと思うのですが、
事情や経緯がありそうな気はします。

ちなみに、RESTEasyをターゲットにした場合にGeneration Gapパターン的なinterfaceOnlyを設定可能なのはjaxrs-specのみのようです。
delegatePatternはないようです ※他にinterfaceOnlyを設定できるのはjaxrs-cxf-cdiのみです

いったんそれぞれのGeneratorで生成して、雰囲気を見てみるとしましょう。

環境

今回の環境はこちら。

$ 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/.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-122-generic", arch: "amd64", family: "unix"

WildFlyは33.0.2.Finalを使います。

準備

まずはベースになるものを用意します。

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>xxxxx</groupId>
    <artifactId>xxxxx</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <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>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>10.0.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.4</version>
            <scope>provided</scope>
        </dependency>

        <!-- あとで -->

    </dependencies>

    <build>
        <finalName>ROOT</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-maven-plugin</artifactId>
                <version>5.0.1.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>33.0.2.Final</version>
                    </discover-provisioning-info>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <version>7.9.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                    <generatorName><!-- あとで --></generatorName>
                    <configOptions>
                        <!-- あとで -->
                    </configOptions>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

依存関係は、Generatorと合わせて後で説明します。

OpenAPI GeneratorはMavenプラグインとして使います。

            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <version>7.9.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                    <generatorName><!-- あとで --></generatorName>
                    <configOptions>
                        <!-- あとで -->
                    </configOptions>
                </configuration>
            </plugin>

設定方法はこちらを見ていけばよいのですが、generateexecutionに設定するとビルド時にOpenAPIドキュメントからソースコード
生成してくれます。

Plugins / Maven

WildFlyWildFly Glowでプロビジョニングすることにします。

Jacksonが入っているのは作成したアプリケーションの都合ですね。

OpenAPIドキュメントは、こちらのエントリーで作成したものをsrc/main/resources配下に置いておきました。

WildFly 33とMicroProfile OpenAPI(SmallRye OpenAPI)でOpenAPIドキュメントを生成する - CLOVER🍀

src/main/resources/openapi.yaml

---
openapi: 3.0.3
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
- name: people
paths:
  /books:
    get:
      tags:
      - book
      summary: 登録された書籍をすべて返却する
      description: 登録された書籍を価格の降順にソートしてすべて返却する
      operationId: findAllBooks
      responses:
        "200":
          description: 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/BookResponse'
  /books/{isbn13}:
    get:
      tags:
      - book
      summary: 指定されたISBNに対応する書籍を取得する
      description: 指定されたISBNに対応する書籍を取得する
      operationId: findBookByIsbn13
      parameters:
      - name: isbn13
        in: path
        description: ISBN
        required: true
        schema:
          maxLength: 14
          minLength: 14
          type: string
        example: 123-4567890123
      responses:
        "200":
          description: 指定された書籍が取得できたことを表す
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookResponse'
        "404":
          description: 指定された書籍が存在しなかったことを表す
    put:
      tags:
      - book
      summary: 指定されたISBNに書籍を登録する
      description: 指定されたISBNに対応する書籍を登録する
      operationId: registerBook
      parameters:
      - name: isbn13
        in: path
        description: ISBN
        required: true
        schema:
          maxLength: 14
          minLength: 14
          type: string
        example: 123-4567890123
      requestBody:
        description: 登録する書籍データ
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookRequest'
        required: true
      responses:
        "200":
          description: 書籍が登録できたことを表す
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookResponse'
        "400":
          description: バリデーションでNGになったことを表す
    delete:
      tags:
      - book
      summary: 指定されたISBNに対応する書籍を削除する
      description: 指定されたISBNに対応する書籍を削除する
      operationId: deleteBookByIsbn13
      parameters:
      - name: isbn13
        in: path
        description: ISBN
        required: true
        schema:
          maxLength: 14
          minLength: 14
          type: string
        example: 123-4567890123
      responses:
        "204":
          description: 書籍が削除されていることを表す
  /people:
    get:
      tags:
      - people
      summary: 登録されている人をすべて返却する
      description: 登録された人をIDの昇順にソートしてすべて返却する
      operationId: findAllPeople
      responses:
        "200":
          description: 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PersonResponse'
    post:
      tags:
      - people
      summary: 人を登録する
      description: 人を登録する
      operationId: registerPerson
      requestBody:
        description: 登録する人のデータ
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PersonRequest'
        required: true
      responses:
        "200":
          description: 人が登録できたことを表す
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonResponse'
        "400":
          description: バリデーションでNGになったことを表す
  /people/{id}:
    get:
      tags:
      - people
      summary: 指定されたIDに対応する人を取得する
      description: 指定されたIDに対応する人を取得する
      operationId: findPersonById
      parameters:
      - name: id
        in: path
        description: ID
        required: true
        schema:
          format: int32
          minimum: 0
          exclusiveMinimum: true
          type: integer
        example: 1
      responses:
        "200":
          description: 指定された人が取得できたことを表す
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonResponse'
        "404":
          description: 指定された人が存在しなかったことを表す
    put:
      tags:
      - people
      summary: 指定されたIDに対応する人を更新する
      description: 指定されたIDに対応する人を更新する
      operationId: updatePerson
      parameters:
      - name: id
        in: path
        description: ID
        required: true
        schema:
          format: int32
          minimum: 0
          exclusiveMinimum: true
          type: integer
        example: 1
      requestBody:
        description: 更新する人のデータ
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PersonRequest'
        required: true
      responses:
        "200":
          description: 人が更新できたことを表す
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PersonResponse'
        "400":
          description: バリデーションでNGになったことを表す
    delete:
      tags:
      - people
      summary: 指定されたIDに対応する人を削除する
      description: 指定されたIDに対応する人を削除する
      operationId: deletePersonById
      parameters:
      - name: id
        in: path
        description: ID
        required: true
        schema:
          format: int32
          minimum: 0
          exclusiveMinimum: true
          type: integer
        example: 1
      responses:
        "204":
          description: 人が削除されていることを表す
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
    PersonRequest:
      description: 登録する人に関するリクエスト
      required:
      - firstName
      - lastName
      - age
      type: object
      properties:
        firstName:
          description: 登録する人の名
          maxLength: 10
          minLength: 1
          type: string
          example: カツオ
        lastName:
          description: 登録する人の姓
          maxLength: 10
          minLength: 1
          type: string
          example: 磯野
        age:
          format: int32
          description: 登録する人の年齢
          minimum: 0
          type: integer
          example: 11
    PersonResponse:
      description: 登録されている人
      type: object
      properties:
        id:
          format: int32
          description: ID
          type: integer
          example: 1
        firstName:
          description:type: string
          example: カツオ
        lastName:
          description:type: string
          example: 磯野
        age:
          format: int32
          description: 年齢
          type: integer
          example: 11

では、各Generatorでソースコードを生成してみます。

ここから先は、まずはOpenAPI Generator Maven Pluginと依存関係に着目して書いていきます。

依存関係のスタートラインはJakarta EEのWeb ProfileとJackson Databindです。

        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>10.0.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.4</version>
            <scope>provided</scope>
        </dependency>

jaxrs-spec Generatorで生成してみる

まずはjaxrs-spec Generatorから。

Documentation for the jaxrs-spec Generator | OpenAPI Generator

OpenAPI Generator Maven Pluginはこのように設定。

                <configuration>
                    <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                    <generatorName>jaxrs-spec</generatorName>
                    <configOptions>
                        <useJakartaEe>true</useJakartaEe>
                        <dateLibrary>java8</dateLibrary>
                        <invokerPackage>org.littlewings.openapi</invokerPackage>
                        <apiPackage>org.littlewings.openapi.api</apiPackage>
                        <modelPackage>org.littlewings.openapi.model</modelPackage>
                    </configOptions>
                </configuration>

generatorName以外は、jaxrs-spec、jaxrs-resteasy、jaxrs-resteasy-eapのいずれでも共通の設定です。以降はこれを基準にしていきます。

ビルド。

$ mvn clean compile

結果は、target/generated-sources/openapiディレクトリ内に出力されます。

$ tree target/generated-sources/openapi
target/generated-sources/openapi
├── README.md
├── pom.xml
└── src
    ├── gen
    │   └── java
    │       └── org
    │           └── littlewings
    │               └── openapi
    │                   ├── RestApplication.java
    │                   ├── RestResourceRoot.java
    │                   ├── api
    │                   │   ├── BooksApi.java
    │                   │   └── PeopleApi.java
    │                   └── model
    │                       ├── BookRequest.java
    │                       ├── BookResponse.java
    │                       ├── PersonRequest.java
    │                       └── PersonResponse.java
    └── main
        └── openapi
            └── openapi.yaml

10 directories, 11 files

JAX-RSApplicationのサブクラス。@ApplicationPathに設定される値は、OpenAPIドキュメントのserversurlのコンテキストパスの
部分が反映されるようです。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/RestApplication.java

package org.littlewings.openapi;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath(RestResourceRoot.APPLICATION_PATH)
public class RestApplication extends Application {

}

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/RestResourceRoot.java

package org.littlewings.openapi;

public class RestResourceRoot {
    public static final String APPLICATION_PATH = "";
}

自動生成されたソースコードをすべて見るのもなんなので、JAX-RSリソースクラスとリクエストのモデルをひとつずつ見ていきましょう。

JAX-RSリソースクラス。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java

package org.littlewings.openapi.api;

import org.littlewings.openapi.model.BookRequest;
import org.littlewings.openapi.model.BookResponse;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;

import io.swagger.annotations.*;

import java.io.InputStream;
import java.util.Map;
import java.util.List;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;

/**
* Represents a collection of functions to interact with the API endpoints.
*/
@Path("/books")
@Api(description = "the books API")
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-10-14T17:58:52.123839482+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public class BooksApi {

    @DELETE
    @Path("/{isbn13}")
    @ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", response = Void.class, tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class)
    })
    public Response deleteBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13) {
        return Response.ok().entity("magic!").build();
    }

    @GET
    @Produces({ "application/json" })
    @ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", response = BookResponse.class, responseContainer = "List", tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List")
    })
    public Response findAllBooks() {
        return Response.ok().entity("magic!").build();
    }

    @GET
    @Path("/{isbn13}")
    @Produces({ "application/json" })
    @ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", response = BookResponse.class, tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class),
        @ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class)
    })
    public Response findBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13) {
        return Response.ok().entity("magic!").build();
    }

    @PUT
    @Path("/{isbn13}")
    @Consumes({ "application/json" })
    @Produces({ "application/json" })
    @ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", response = BookResponse.class, tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class),
        @ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class)
    })
    public Response registerBook(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13,@Valid @NotNull BookRequest bookRequest) {
        return Response.ok().entity("magic!").build();
    }
}

メソッドの戻り値がResponseなので、レスポンスの型をOpenAPIドキュメントの定義で強制できませんね。ただ、Responseを使わないと
レスポンスヘッダーの設定ができなかったりするので微妙なところです。

モデル。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/model/BookRequest.java

package org.littlewings.openapi.model;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.time.LocalDate;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;

import io.swagger.annotations.*;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.annotation.JsonTypeName;

/**
 * 登録する書籍に関するリクエスト
 **/
@ApiModel(description = "登録する書籍に関するリクエスト")
@JsonTypeName("BookRequest")
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-10-19T00:03:10.237263859+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public class BookRequest   {
  private String title;
  private Integer price;
  private LocalDate publishDate;

  /**
   * 登録する書籍のタイトル
   **/
  public BookRequest title(String title) {
    this.title = title;
    return this;
  }


  @ApiModelProperty(example = "Javaの本", required = true, value = "登録する書籍のタイトル")
  @JsonProperty("title")
  @NotNull  @Size(min=1,max=100)public String getTitle() {
    return title;
  }

  @JsonProperty("title")
  public void setTitle(String title) {
    this.title = title;
  }

  /**
   * 登録する書籍の価格
   * minimum: 0
   **/
  public BookRequest price(Integer price) {
    this.price = price;
    return this;
  }


  @ApiModelProperty(example = "1500", required = true, value = "登録する書籍の価格")
  @JsonProperty("price")
  @NotNull  @Min(0)public Integer getPrice() {
    return price;
  }

  @JsonProperty("price")
  public void setPrice(Integer price) {
    this.price = price;
  }

  /**
   * 登録する書籍の出版日
   **/
  public BookRequest publishDate(LocalDate publishDate) {
    this.publishDate = publishDate;
    return this;
  }


  @ApiModelProperty(example = "Sun Oct 13 09:00:00 JST 2024", required = true, value = "登録する書籍の出版日")
  @JsonProperty("publishDate")
  @NotNull public LocalDate getPublishDate() {
    return publishDate;
  }

  @JsonProperty("publishDate")
  public void setPublishDate(LocalDate publishDate) {
    this.publishDate = publishDate;
  }


  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    BookRequest bookRequest = (BookRequest) o;
    return Objects.equals(this.title, bookRequest.title) &&
        Objects.equals(this.price, bookRequest.price) &&
        Objects.equals(this.publishDate, bookRequest.publishDate);
  }

  @Override
  public int hashCode() {
    return Objects.hash(title, price, publishDate);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("class BookRequest {\n");

    sb.append("    title: ").append(toIndentedString(title)).append("\n");
    sb.append("    price: ").append(toIndentedString(price)).append("\n");
    sb.append("    publishDate: ").append(toIndentedString(publishDate)).append("\n");
    sb.append("}");
    return sb.toString();
  }

  /**
   * Convert the given object to string with each line indented by 4 spaces
   * (except the first line).
   */
  private String toIndentedString(Object o) {
    if (o == null) {
      return "null";
    }
    return o.toString().replace("\n", "\n    ");
  }


}

Bean Validationのアノテーションですが、この後でもとになったエントリーのテストコードを実行して気づいたのですが
exclusiveMinimum: trueが無視されているようです。なので、0を許容しないはずはずのリクエストパラメーターのいくつかが0を
許容するようになってしまっています。

このコードをビルドするには、以下の依存関係を追加する必要があります。

        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.6.14</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.15.4</version>
            <scope>provided</scope>
        </dependency>

こちらのページを見ると、io.swagger.core.v3:swagger-annotationsを追加すればよさそうに見えるのですが、jaxrs-specで生成される
ソースコードはSwagger v3 annotationsには対応していないようです。

Plugins / Maven / Dependencies

また、この2つの依存関係はjaxrs-resteasy、jaxrs-resteasy-eapでも必要になります。

interfaceOnlyにしてみる

今回の自動生成結果ではJAX-RSリソースクラスに関してもクラスそのものが生成されたので、OpenAPIドキュメントの定義が変わったりして
自動生成をもう1度行うとソースコードのマージをすることになって困ります。

jaxrs-specではinterfaceOnlytrueにすると、JAX-RSリソースに関してはインターフェースを生成するようになります。

                <configuration>
                    <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                    <generatorName>jaxrs-spec</generatorName>
                    <configOptions>
                        <useJakartaEe>true</useJakartaEe>
                        <dateLibrary>java8</dateLibrary>
                        <invokerPackage>org.littlewings.openapi</invokerPackage>
                        <apiPackage>org.littlewings.openapi.api</apiPackage>
                        <modelPackage>org.littlewings.openapi.model</modelPackage>

                        <interfaceOnly>true</interfaceOnly>
                    </configOptions>
                </configuration>

結果はこちら。

target/generated-sources/openapi
├── README.md
├── pom.xml
└── src
    ├── gen
    │   └── java
    │       └── org
    │           └── littlewings
    │               └── openapi
    │                   ├── RestApplication.java
    │                   ├── RestResourceRoot.java
    │                   ├── api
    │                   │   ├── BooksApi.java
    │                   │   └── PeopleApi.java
    │                   └── model
    │                       ├── BookRequest.java
    │                       ├── BookResponse.java
    │                       ├── PersonRequest.java
    │                       └── PersonResponse.java
    └── main
        └── openapi
            └── openapi.yaml

10 directories, 11 files

パッと見では結果は変わりませんが、JAX-RSリソースについては生成結果がインターフェースになっています。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java

package org.littlewings.openapi.api;

import org.littlewings.openapi.model.BookRequest;
import org.littlewings.openapi.model.BookResponse;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;

import io.swagger.annotations.*;

import java.io.InputStream;
import java.util.Map;
import java.util.List;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;

/**
* Represents a collection of functions to interact with the API endpoints.
*/
@Path("/books")
@Api(description = "the books API")
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-10-14T18:13:42.623922339+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public interface BooksApi {

    /**
     * 指定されたISBNに対応する書籍を削除する
     *
     * @param isbn13 ISBN
     * @return 書籍が削除されていることを表す
     */
    @DELETE
    @Path("/{isbn13}")
    @ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class) })
    void deleteBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13);


    /**
     * 登録された書籍を価格の降順にソートしてすべて返却する
     *
     * @return 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す
     */
    @GET
    @Produces({ "application/json" })
    @ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List") })
    List<BookResponse> findAllBooks();


    /**
     * 指定されたISBNに対応する書籍を取得する
     *
     * @param isbn13 ISBN
     * @return 指定された書籍が取得できたことを表す
     * @return 指定された書籍が存在しなかったことを表す
     */
    @GET
    @Path("/{isbn13}")
    @Produces({ "application/json" })
    @ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class),
        @ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class) })
    BookResponse findBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13);


    /**
     * 指定されたISBNに対応する書籍を登録する
     *
     * @param isbn13 ISBN
     * @param bookRequest 登録する書籍データ
     * @return 書籍が登録できたことを表す
     * @return バリデーションでNGになったことを表す
     */
    @PUT
    @Path("/{isbn13}")
    @Consumes({ "application/json" })
    @Produces({ "application/json" })
    @ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class),
        @ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class) })
    BookResponse registerBook(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13,@Valid @NotNull BookRequest bookRequest);

}

あとは、このインターフェースを実装したJAX-RSリソースクラスを作成すればよいことになります。

ところで、メソッドの戻り値の型がJAX-RSResponseではなく具体的な型になっています。レスポンスヘッダーを扱いたい場合などは
このままだと困ることになるので、そういう場合はreturnResponsetrueにすると

                <configuration>
                    <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                    <generatorName>jaxrs-spec</generatorName>
                    <configOptions>
                        <useJakartaEe>true</useJakartaEe>
                        <dateLibrary>java8</dateLibrary>
                        <invokerPackage>org.littlewings.openapi</invokerPackage>
                        <apiPackage>org.littlewings.openapi.api</apiPackage>
                        <modelPackage>org.littlewings.openapi.model</modelPackage>

                        <interfaceOnly>true</interfaceOnly>
                        <returnResponse>true</returnResponse>
                    </configOptions>
                </configuration>

生成されるのはインターフェースのままで、メソッドの戻り値の型がJAX-RSResponseになります。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java

package org.littlewings.openapi.api;

import org.littlewings.openapi.model.BookRequest;
import org.littlewings.openapi.model.BookResponse;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;

import io.swagger.annotations.*;

import java.io.InputStream;
import java.util.Map;
import java.util.List;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;

/**
* Represents a collection of functions to interact with the API endpoints.
*/
@Path("/books")
@Api(description = "the books API")
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-10-14T18:16:23.044032831+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public interface BooksApi {

    /**
     * 指定されたISBNに対応する書籍を削除する
     *
     * @param isbn13 ISBN
     * @return 書籍が削除されていることを表す
     */
    @DELETE
    @Path("/{isbn13}")
    @ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class) })
    Response deleteBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13);


    /**
     * 登録された書籍を価格の降順にソートしてすべて返却する
     *
     * @return 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す
     */
    @GET
    @Produces({ "application/json" })
    @ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List") })
    Response findAllBooks();


    /**
     * 指定されたISBNに対応する書籍を取得する
     *
     * @param isbn13 ISBN
     * @return 指定された書籍が取得できたことを表す
     * @return 指定された書籍が存在しなかったことを表す
     */
    @GET
    @Path("/{isbn13}")
    @Produces({ "application/json" })
    @ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class),
        @ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class) })
    Response findBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13);


    /**
     * 指定されたISBNに対応する書籍を登録する
     *
     * @param isbn13 ISBN
     * @param bookRequest 登録する書籍データ
     * @return 書籍が登録できたことを表す
     * @return バリデーションでNGになったことを表す
     */
    @PUT
    @Path("/{isbn13}")
    @Consumes({ "application/json" })
    @Produces({ "application/json" })
    @ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", tags={ "book" })
    @ApiResponses(value = {
        @ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class),
        @ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class) })
    Response registerBook(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13,@Valid @NotNull BookRequest bookRequest);

}

なお、interfaceOnlyreturnResponseもjaxrs-resteasy、jaxrs-resteasy-eapでは指定できません。

jaxrs-resteasy

次は、jaxrs-resteasyです。

Documentation for the jaxrs-resteasy Generator | OpenAPI Generator

OpenAPI Generator Maven Pluginの設定はこうしました。

                <configuration>
                    <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                    <generatorName>jaxrs-resteasy</generatorName>
                    <configOptions>
                        <useJakartaEe>true</useJakartaEe>
                        <dateLibrary>java8</dateLibrary>
                        <invokerPackage>org.littlewings.openapi</invokerPackage>
                        <apiPackage>org.littlewings.openapi.api</apiPackage>
                        <modelPackage>org.littlewings.openapi.model</modelPackage>
                    </configOptions>
                </configuration>

生成結果はこうなりました。

target/generated-sources/openapi
target/generated-sources/openapi
├── README.md
├── build.gradle
├── pom.xml
├── settings.gradle
└── src
    ├── gen
    │   └── java
    │       └── org
    │           └── littlewings
    │               └── openapi
    │                   ├── JacksonConfig.java
    │                   ├── RFC3339DateFormat.java
    │                   ├── RestApplication.java
    │                   ├── StringUtil.java
    │                   ├── api
    │                   │   ├── ApiException.java
    │                   │   ├── ApiOriginFilter.java
    │                   │   ├── ApiResponseMessage.java
    │                   │   ├── BooksApi.java
    │                   │   ├── BooksApiService.java
    │                   │   ├── LocalDateProvider.java
    │                   │   ├── NotFoundException.java
    │                   │   ├── OffsetDateTimeProvider.java
    │                   │   ├── PeopleApi.java
    │                   │   └── PeopleApiService.java
    │                   └── model
    │                       ├── BookRequest.java
    │                       ├── BookResponse.java
    │                       ├── PersonRequest.java
    │                       └── PersonResponse.java
    └── main
        ├── java
        │   └── org
        │       └── littlewings
        │           └── openapi
        │               └── api
        │                   └── impl
        │                       ├── BooksApiServiceImpl.java
        │                       └── PeopleApiServiceImpl.java
        └── webapp
            └── WEB-INF
                ├── jboss-web.xml
                └── web.xml

17 directories, 26 files

jaxrs-specとはだいぶ雰囲気が変わりました。

web.xmlなども生成されていますね。

target/generated-sources/openapi/src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:j2ee="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee    http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

  <filter>
    <filter-name>ApiOriginFilter</filter-name>
    <filter-class>org.littlewings.openapi.api.ApiOriginFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>ApiOriginFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

target/generated-sources/openapi/src/main/webapp/WEB-INF/jboss-web.xml

<jboss-web>
    <context-root></context-root>
</jboss-web>

Applicationのサブクラスは、少し簡素になりました。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/RestApplication.java

package org.littlewings.openapi;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("")
public class RestApplication extends Application {

}

生成されるJavaソースコードは大量に増えているのですが、Serviceというものが増えているのがポイントでしょうか。

└── src
    ├── gen
    │   └── java
    │       └── org
    │           └── littlewings
    │               └── openapi
    │                   ├── JacksonConfig.java
    │                   ├── RFC3339DateFormat.java
    │                   ├── RestApplication.java
    │                   ├── StringUtil.java
    │                   ├── api
    │                   │   ├── ApiException.java
    │                   │   ├── ApiOriginFilter.java
    │                   │   ├── ApiResponseMessage.java
    │                   │   ├── BooksApi.java
    │                   │   ├── BooksApiService.java
    │                   │   ├── LocalDateProvider.java
    │                   │   ├── NotFoundException.java
    │                   │   ├── OffsetDateTimeProvider.java
    │                   │   ├── PeopleApi.java
    │                   │   └── PeopleApiService.java
    │                   └── model
    │                       ├── BookRequest.java
    │                       ├── BookResponse.java
    │                       ├── PersonRequest.java
    │                       └── PersonResponse.java

JAX-RSリソースクラスの定義を見ると、Serviceクラスをインジェクションして委譲する実装になっています。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java

package org.littlewings.openapi.api;

import org.littlewings.openapi.model.*;
import org.littlewings.openapi.api.BooksApiService;

import io.swagger.annotations.ApiParam;
import io.swagger.jaxrs.*;

import org.littlewings.openapi.model.BookRequest;
import org.littlewings.openapi.model.BookResponse;

import java.util.Map;
import java.util.List;
import org.littlewings.openapi.api.NotFoundException;

import java.io.InputStream;

import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.*;
import jakarta.inject.Inject;

import jakarta.validation.constraints.*;
import jakarta.validation.Valid;

@Path("/books")


@io.swagger.annotations.Api(description = "the books API")
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public class BooksApi  {

    @Inject BooksApiService service;

    @DELETE
    @Path("/{isbn13}")


    @io.swagger.annotations.ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", response = Void.class, tags={ "book", })
    @io.swagger.annotations.ApiResponses(value = {
        @io.swagger.annotations.ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class) })
    public Response deleteBookByIsbn13( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@Context SecurityContext securityContext)
    throws NotFoundException {
        return service.deleteBookByIsbn13(isbn13,securityContext);
    }
    @GET


    @Produces({ "application/json" })
    @io.swagger.annotations.ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", response = BookResponse.class, responseContainer = "List", tags={ "book", })
    @io.swagger.annotations.ApiResponses(value = {
        @io.swagger.annotations.ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List") })
    public Response findAllBooks(@Context SecurityContext securityContext)
    throws NotFoundException {
        return service.findAllBooks(securityContext);
    }
    @GET
    @Path("/{isbn13}")

    @Produces({ "application/json" })
    @io.swagger.annotations.ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", response = BookResponse.class, tags={ "book", })
    @io.swagger.annotations.ApiResponses(value = {
        @io.swagger.annotations.ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class),

        @io.swagger.annotations.ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class) })
    public Response findBookByIsbn13( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@Context SecurityContext securityContext)
    throws NotFoundException {
        return service.findBookByIsbn13(isbn13,securityContext);
    }
    @PUT
    @Path("/{isbn13}")
    @Consumes({ "application/json" })
    @Produces({ "application/json" })
    @io.swagger.annotations.ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", response = BookResponse.class, tags={ "book", })
    @io.swagger.annotations.ApiResponses(value = {
        @io.swagger.annotations.ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class),

        @io.swagger.annotations.ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class) })
    public Response registerBook( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@ApiParam(value = "登録する書籍データ" ,required=true) @NotNull @Valid BookRequest bookRequest,@Context SecurityContext securityContext)
    throws NotFoundException {
        return service.registerBook(isbn13,bookRequest,securityContext);
    }
}

Serviceはインターフェースですね。戻り値は一律JAX-RSResponseです。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApiService.java

package org.littlewings.openapi.api;

import org.littlewings.openapi.api.*;
import org.littlewings.openapi.model.*;


import org.littlewings.openapi.model.BookRequest;
import org.littlewings.openapi.model.BookResponse;

import java.util.List;
import org.littlewings.openapi.api.NotFoundException;

import java.io.InputStream;

import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;

@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public interface BooksApiService {
      Response deleteBookByIsbn13(String isbn13,SecurityContext securityContext)
      throws NotFoundException;
      Response findAllBooks(SecurityContext securityContext)
      throws NotFoundException;
      Response findBookByIsbn13(String isbn13,SecurityContext securityContext)
      throws NotFoundException;
      Response registerBook(String isbn13,BookRequest bookRequest,SecurityContext securityContext)
      throws NotFoundException;
}

実装クラスはスケルトンが生成されるようなので、こちらを取り込んで修正していく使い方をイメージしているような気がします。

target/generated-sources/openapi/src/main/java/org/littlewings/openapi/api/impl/BooksApiServiceImpl.java

package org.littlewings.openapi.api.impl;

import org.littlewings.openapi.api.*;
import org.littlewings.openapi.model.*;


import org.littlewings.openapi.model.BookRequest;
import org.littlewings.openapi.model.BookResponse;

import java.util.List;
import org.littlewings.openapi.api.NotFoundException;

import java.io.InputStream;

import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;

@RequestScoped
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public class BooksApiServiceImpl implements BooksApiService {
      public Response deleteBookByIsbn13(String isbn13,SecurityContext securityContext)
      throws NotFoundException {
      // do some magic!
      return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build();
  }
      public Response findAllBooks(SecurityContext securityContext)
      throws NotFoundException {
      // do some magic!
      return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build();
  }
      public Response findBookByIsbn13(String isbn13,SecurityContext securityContext)
      throws NotFoundException {
      // do some magic!
      return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build();
  }
      public Response registerBook(String isbn13,BookRequest bookRequest,SecurityContext securityContext)
      throws NotFoundException {
      // do some magic!
      return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build();
  }
}

ちなみに、この実装クラスが生成されるのはtarget/generated-sources/openapi/src/main/java配下です。

インターフェースやモデルが生成されていたのはtarget/generated-sources/openapi/src/gen/java配下で、こちらは修正しないソースコード
位置づけかなと思います。

モデルはjaxrs-specと大差なかったので省略。

その他のクラスをいくつか。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/JacksonConfig.java

package org.littlewings.openapi;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;

@Provider
public class JacksonConfig implements ContextResolver<ObjectMapper> {
    private final ObjectMapper objectMapper;

    public JacksonConfig() throws Exception {

        objectMapper = new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .setDateFormat(new RFC3339DateFormat());
    }

    public ObjectMapper getContext(Class<?> arg0) {
        return objectMapper;
    }
}

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/RFC3339DateFormat.java

package org.littlewings.openapi;

import com.fasterxml.jackson.databind.util.StdDateFormat;

import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

public class RFC3339DateFormat extends DateFormat {
  private static final long serialVersionUID = 1L;
  private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC");

  private final StdDateFormat fmt = new StdDateFormat()
          .withTimeZone(TIMEZONE_Z)
          .withColonInTimeZone(true);

  public RFC3339DateFormat() {
    this.calendar = new GregorianCalendar();
  }

  @Override
  public Date parse(String source, ParsePosition pos) {
    return fmt.parse(source, pos);
  }

  @Override
  public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
    return fmt.format(date, toAppendTo, fieldPosition);
  }

  @Override
  public Object clone() {
    return this;
  }
}

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/StringUtil.java

package org.littlewings.openapi;

@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public class StringUtil {
  /**
   * Check if the given array contains the given value (with case-insensitive comparison).
   *
   * @param array The array
   * @param value The value to search
   * @return true if the array contains the value
   */
  public static boolean containsIgnoreCase(String[] array, String value) {
    for (String str : array) {
      if (value == null && str == null) return true;
      if (value != null && value.equalsIgnoreCase(str)) return true;
    }
    return false;
  }

  /**
   * Join an array of strings with the given separator.
   * <p>
   * Note: This might be replaced by utility method from commons-lang or guava someday
   * if one of those libraries is added as dependency.
   * </p>
   *
   * @param array     The array of strings
   * @param separator The separator
   * @return the resulting string
   */
  public static String join(String[] array, String separator) {
    int len = array.length;
    if (len == 0) return "";

    StringBuilder out = new StringBuilder();
    out.append(array[0]);
    for (int i = 1; i < len; i++) {
      out.append(separator).append(array[i]);
    }
    return out.toString();
  }
}

Jackson向けのクラスが生成されていますね。

このGeneratorの特徴(?)は、apiパッケージ配下に生成されるクラスが多いことですね。

こういうのもできたりします。

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/ApiResponseMessage.java

package org.littlewings.openapi.api;

import jakarta.xml.bind.annotation.XmlTransient;

@jakarta.xml.bind.annotation.XmlRootElement
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public class ApiResponseMessage {
    public static final int ERROR = 1;
    public static final int WARNING = 2;
    public static final int INFO = 3;
    public static final int OK = 4;
    public static final int TOO_BUSY = 5;

    int code;
    String type;
    String message;

    public ApiResponseMessage(){}

    public ApiResponseMessage(int code, String message){
        this.code = code;
        switch(code){
        case ERROR:
            setType("error");
            break;
        case WARNING:
            setType("warning");
            break;
        case INFO:
            setType("info");
            break;
        case OK:
            setType("ok");
            break;
        case TOO_BUSY:
            setType("too busy");
            break;
        default:
            setType("unknown");
            break;
        }
        this.message = message;
    }

    @XmlTransient
    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

JAXBが使われたりしていますが…。

そんなわけもあって、このソースコードをビルドするには以下の依存関係が必要です。

        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.6.14</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.15.4</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-jaxrs</artifactId>
            <version>1.6.14</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>jakarta.xml.bind</groupId>
            <artifactId>jakarta.xml.bind-api</artifactId>
            <version>4.0.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.4</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.15.4</version>
            <scope>provided</scope>
        </dependency>

swagger-jaxrsにもSwagger v3版があるのですが、古いバージョンを使っています。

jaxrs-resteasy-eap

最後はjaxrs-resteasy-eapです。名前からすると、JBoss EAPまたはWildFlyにデプロイすることを前提にしたGeneratorになるのでしょうか?
※その割にはjaxrs-resteasyでもjboss-web.xmlが生成されたりしていますが

Documentation for the jaxrs-resteasy-eap Generator | OpenAPI Generator

                <configuration>
                    <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                    <generatorName>jaxrs-resteasy-eap</generatorName>
                    <configOptions>
                        <useJakartaEe>true</useJakartaEe>
                        <dateLibrary>java8</dateLibrary>
                        <invokerPackage>org.littlewings.openapi</invokerPackage>
                        <apiPackage>org.littlewings.openapi.api</apiPackage>
                        <modelPackage>org.littlewings.openapi.model</modelPackage>
                    </configOptions>
                </configuration>

自動生成結果はこちらです。

target/generated-sources/openapi
├── README.md
├── build.gradle
├── pom.xml
├── settings.gradle
└── src
    ├── gen
    │   └── java
    │       └── org
    │           └── littlewings
    │               └── openapi
    │                   ├── api
    │                   │   ├── BooksApi.java
    │                   │   └── PeopleApi.java
    │                   └── model
    │                       ├── BookRequest.java
    │                       ├── BookResponse.java
    │                       ├── PersonRequest.java
    │                       └── PersonResponse.java
    └── main
        ├── java
        │   └── org
        │       └── littlewings
        │           └── openapi
        │               ├── JacksonConfig.java
        │               ├── RestApplication.java
        │               └── api
        │                   └── impl
        │                       ├── BooksApiServiceImpl.java
        │                       └── PeopleApiServiceImpl.java
        └── webapp
            └── WEB-INF
                ├── jboss-web.xml
                └── web.xml

17 directories, 16 files

jaxrs-resteasyとはだいぶ結果が変わりました…。

jboss-web.xmlはjaxrs-resteasyと同じでしたが、web.xmlは異なる結果になりました。

target/generated-sources/openapi/src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:j2ee="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee    http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <context-param>
        <param-name>resteasy.providers</param-name>
        <param-value>org.littlewings.openapi.JacksonConfig</param-value>
    </context-param>
</web-app>

Applicationのサブクラスも実装側のディレクトリーに配置されるようになっているのと、定義もけっこう変わっています。

target/generated-sources/openapi/src/main/java/org/littlewings/openapi/RestApplication.java

package org.littlewings.openapi;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

import java.util.Set;
import java.util.HashSet;

import org.littlewings.openapi.api.impl.BooksApiServiceImpl;
import org.littlewings.openapi.api.impl.PeopleApiServiceImpl;

@ApplicationPath("")
public class RestApplication extends Application {


    public Set<Class<?>> getClasses() {
        Set<Class<?>> resources = new HashSet<Class<?>>();
        resources.add(BooksApiServiceImpl.class);
        resources.add(PeopleApiServiceImpl.class);

        return resources;
    }




}

Serviceの実装クラスをJAX-RSリソースクラスとして追加していますね。

Apiという名前のクラスはどうなったのでしょうか?

target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java

package org.littlewings.openapi.api;

import org.littlewings.openapi.model.*;

import io.swagger.annotations.ApiParam;
import io.swagger.jaxrs.*;

import org.littlewings.openapi.model.BookRequest;
import org.littlewings.openapi.model.BookResponse;

import java.util.List;
import java.util.Map;

import java.io.InputStream;

import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.*;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;

@Path("/books")


@io.swagger.annotations.Api(description = "the books API")
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyEapServerCodegen", date = "2024-10-14T18:37:10.861562623+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public interface BooksApi  {

    @DELETE
    @Path("/{isbn13}")


    @io.swagger.annotations.ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", response = Void.class, tags={ "book", })
    @io.swagger.annotations.ApiResponses(value = {
        @io.swagger.annotations.ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class) })
    public Response deleteBookByIsbn13( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@Context SecurityContext securityContext);
    @GET


    @Produces({ "application/json" })
    @io.swagger.annotations.ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", response = BookResponse.class, responseContainer = "List", tags={ "book", })
    @io.swagger.annotations.ApiResponses(value = {
        @io.swagger.annotations.ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List") })
    public Response findAllBooks(@Context SecurityContext securityContext);
    @GET
    @Path("/{isbn13}")

    @Produces({ "application/json" })
    @io.swagger.annotations.ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", response = BookResponse.class, tags={ "book", })
    @io.swagger.annotations.ApiResponses(value = {
        @io.swagger.annotations.ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class),

        @io.swagger.annotations.ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class) })
    public Response findBookByIsbn13( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@Context SecurityContext securityContext);
    @PUT
    @Path("/{isbn13}")
    @Consumes({ "application/json" })
    @Produces({ "application/json" })
    @io.swagger.annotations.ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", response = BookResponse.class, tags={ "book", })
    @io.swagger.annotations.ApiResponses(value = {
        @io.swagger.annotations.ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class),

        @io.swagger.annotations.ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class) })
    public Response registerBook( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@ApiParam(value = "登録する書籍データ" ,required=true) @NotNull @Valid BookRequest bookRequest,@Context SecurityContext securityContext);
}

こちらはインターフェースのようです。

Serviceの実装クラスを見ると、インターフェースとして生成されたApiを実装する形態なっているようです。

target/generated-sources/openapi/src/main/java/org/littlewings/openapi/api/impl/BooksApiServiceImpl.java

package org.littlewings.openapi.api.impl;

import org.littlewings.openapi.api.*;
import org.littlewings.openapi.model.*;


import org.littlewings.openapi.model.BookRequest;
import org.littlewings.openapi.model.BookResponse;

import java.util.List;

import java.io.InputStream;

import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;

@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyEapServerCodegen", date = "2024-10-14T18:37:10.861562623+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0")
public class BooksApiServiceImpl implements BooksApi {
      public Response deleteBookByIsbn13(String isbn13,SecurityContext securityContext) {
      // do some magic!
      return Response.ok().build();
  }
      public Response findAllBooks(SecurityContext securityContext) {
      // do some magic!
      return Response.ok().build();
  }
      public Response findBookByIsbn13(String isbn13,SecurityContext securityContext) {
      // do some magic!
      return Response.ok().build();
  }
      public Response registerBook(String isbn13,BookRequest bookRequest,SecurityContext securityContext) {
      // do some magic!
      return Response.ok().build();
  }
}

Jackson向けの設定も、実装側のディレクトリーに置かれるようになりました。

target/generated-sources/openapi/src/main/java/org/littlewings/openapi/JacksonConfig.java

package org.littlewings.openapi;

import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

@Provider
@Produces(MediaType.APPLICATION_JSON)
public class JacksonConfig implements ContextResolver<ObjectMapper> {

    private static final Logger LOG = LoggerFactory.getLogger(JacksonConfig.class);

    private ObjectMapper objectMapper;

    public JacksonConfig() throws Exception {
        this.objectMapper = new ObjectMapper();

        this.objectMapper.registerModule(new JavaTimeModule());

        // sample to convert any DateTime to readable timestamps
        //this.objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
    }

    public ObjectMapper getContext(Class<?> objectType) {
        return objectMapper;
    }
}

jaxrs-resteayよりもファイルが減ったのはいいのですが、ちょっと不思議な生成結果のような気がします…。

jaxrs-resteasy-eapで生成したソースコードをビルドするのに追加が必要な依存関係は、こちらになります。

        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.6.14</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.15.4</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-jaxrs</artifactId>
            <version>1.6.14</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.16</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.4</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.15.4</version>
            <scope>provided</scope>
        </dependency>

なぜかSLF4Jに依存しているのですが…JBoss Loggingに

jaxrs-specを使ってREST APIを実装する

と、ここまでいろいろと試してみましたが、今回は1番シンプルそう(?)なjaxrs-specを使うことにします。

interfaceOnlytrueにして、sourceFolderはデフォルトのsrc/gen/javaからsrc/main/javaにしておきました。

                <configuration>
                    <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
                    <generatorName>jaxrs-spec</generatorName>
                    <configOptions>
                        <!-- デフォルトは src/gen/java -->
                        <sourceFolder>src/main/java</sourceFolder>
                        <useJakartaEe>true</useJakartaEe>
                        <dateLibrary>java8</dateLibrary>
                        <invokerPackage>org.littlewings.openapi</invokerPackage>
                        <apiPackage>org.littlewings.openapi.api</apiPackage>
                        <modelPackage>org.littlewings.openapi.model</modelPackage>

                        <interfaceOnly>true</interfaceOnly>
                        <returnResponse>false</returnResponse>

                        <generateBuilders>true</generateBuilders>
                    </configOptions>
                </configuration>

sourceFoldersrc/main/javaにしておくと、自動生成されたソースコードをそのままビルド対象に含めることができます。
生成されるソースコードはすべて修正しないものになるので、これでいいかなと。

あとはモデルのインスタンスを構築するためのビルダーを生成する、generateBuilderstrueにしておきました。

実装したJAX-RSリソースクラス。

src/main/java/org/littlewings/openapi/api/BooksApiImpl.java

package org.littlewings.openapi.api;

import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import jakarta.enterprise.context.ApplicationScoped;
import org.littlewings.openapi.model.BookRequest;
import org.littlewings.openapi.model.BookResponse;

@ApplicationScoped
public class BooksApiImpl implements BooksApi {
    private ConcurrentMap<String, BookResponse> store = new ConcurrentHashMap<>();

    @Override
    public void deleteBookByIsbn13(String isbn13) {
        store.remove(isbn13);
    }

    @Override
    public List<BookResponse> findAllBooks() {
        return store.values().stream().sorted(Comparator.comparing(BookResponse::getPrice).reversed()).toList();
    }

    @Override
    public BookResponse findBookByIsbn13(String isbn13) {
        return store.get(isbn13);
    }

    @Override
    public BookResponse registerBook(String isbn13, BookRequest bookRequest) {
        return store.compute(isbn13, (i, before) ->
                BookResponse.builder()
                        .isbn13(i)
                        .title(bookRequest.getTitle())
                        .price(bookRequest.getPrice())
                        .publishDate(bookRequest.getPublishDate())
                        .build()
        );
    }
}

src/main/java/org/littlewings/openapi/api/PeopleApiImpl.java

package org.littlewings.openapi.api;

import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import jakarta.enterprise.context.ApplicationScoped;
import org.littlewings.openapi.model.PersonRequest;
import org.littlewings.openapi.model.PersonResponse;

@ApplicationScoped
public class PeopleApiImpl implements PeopleApi {
    private ConcurrentMap<Integer, PersonResponse> store = new ConcurrentHashMap<>();

    @Override
    public void deletePersonById(Integer id) {
        store.remove(id);
    }

    @Override
    public List<PersonResponse> findAllPeople() {
        return store.values().stream().sorted(Comparator.comparing(PersonResponse::getId)).toList();
    }

    @Override
    public PersonResponse findPersonById(Integer id) {
        return store.get(id);
    }

    @Override
    public PersonResponse registerPerson(PersonRequest personRequest) {
        String identity = personRequest.getFirstName() + ":" + personRequest.getLastName();

        if (store.values().stream().filter(r -> identity.equals(r.getFirstName() + ":" + r.getLastName())).findFirst().isEmpty()) {
            return store.compute(store.size() + 1, (i, before) ->
                    PersonResponse.builder()
                            .id(i)
                            .firstName(personRequest.getFirstName())
                            .lastName(personRequest.getLastName())
                            .age(personRequest.getAge())
                            .build()
            );
        }

        return store.values().stream().filter(r -> identity.equals(r.getFirstName() + ":" + r.getLastName())).findFirst().get();
    }

    @Override
    public PersonResponse updatePerson(Integer id, PersonRequest personRequest) {
        return store.compute(id, (i, before) ->
                PersonResponse.builder()
                        .id(i)
                        .firstName(personRequest.getFirstName())
                        .lastName(personRequest.getLastName())
                        .age(personRequest.getAge())
                        .build()
        );
    }
}

JAX-RSリソースクラスに関しては、こちらで作成したものをOpenAPI Generatorで生成されたモデルを利用するように修正したものです。

WildFly 33とMicroProfile OpenAPI(SmallRye OpenAPI)でOpenAPIドキュメントを生成する - CLOVER🍀

Jacksonの設定。最初にJackson Databindの依存関係を入れていたのは、これが理由ですね。

src/main/java/org/littlewings/openapi/provider/ObjectMapperProvider.java

package org.littlewings.openapi.provider;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;

@Provider
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
    @Override
    public ObjectMapper getContext(Class<?> type) {
        return new ObjectMapper().findAndRegisterModules();
    }
}

テストについてもこちらで用意したものを使いましたが、exclusiveMinimum: trueが考慮されていないので0を境界にするテストは
通りませんでした。

WildFly 33とMicroProfile OpenAPI(SmallRye OpenAPI)でOpenAPIドキュメントを生成する - CLOVER🍀

とりあえず、雰囲気はわかったのでこんなところでよいでしょう。

おわりに

OpenAPI GeneratorでJAX-RS(RESTEasy)のサーバーサイドのソースコードを生成してみました。

JAX-RS向けのGeneratorはたくさんあるし、使ってみるとSwagger v3 annotationに移行していなかったり、バリデーションで境界値を踏んだりと
いろいろありましたが、Generatorは大量にあるのでメンテナンスも大変だろうなぁと…。

とはいえ、JAX-RSのGeneratorはたくさんあるので使い分けくらいはもう少し説明してくれてもいいのではないかと…。

次に使う時は、それほど迷わないでしょう、きっと。