【Spring Boot】OpenAPIを使用したAPI仕様書の作成方法

## はじめに

Spring Boot　×　OpenAPIでAPI仕様書を作成しました。

その際、どのようなアノテーションをつけられるか、アノテーションのプロパティには何を記述すべきか、といったことに悩み、調べてみたのですが公式ドキュメント以外あまり情報がありませんでした。

結果、ほぼ公式ドキュメントを読みながら作成したので、備忘録として使い方を残しておきます。

### 環境

- OS: Windows11
- IDE: IntelliJ IDEA 2024.2.4 (Community Edition)
- Java: "21.0.5" 2024-10-15 LTS
- SpringBoot: "3.3.5"
- springdoc-openapi-starter-webmvc-ui: 2.6.0

### この記事で扱わない範囲

- ライブラリのインストール方法詳細
- 多種類あるOpenApiライブラリの違いについての解説
- アノテーションを利用しないOpenAPIの使い方
- mainメソッドがあるファイルで使用する``@OpenAPIDefinition``の詳細な使い方

## 使い方詳細

### 導入方法

- MavenRepositoryからライブラリをインストール
    - https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui
- Spring Bootのサーバーを起動
- `[http://localhost:8080/swagger-ui/index.html](http://localhost:8080/swagger-ui/index.html#/student-controller/registerStudent)`　にアクセス
    - ポート番号を変えている場合は8080を修正してください

### 全体像

- アノテーションをつけて、属性値を記述していく。成果物のイメージは以下を参照していただけるとわかりやすいかと思います
    - https://omi354.github.io/portfolio-student-management/
- アノテーションをつける箇所は主に以下の3つ
    - mainメソッドのあるクラス
    - Controller
    - データモデル
- OpenAPI用のアノテーション内に記述した内容は、仕様書に反映されるだけで、実際の機能の振る舞いを変えるわけではありません

### mainメソッドのあるクラスへの記載

- 全体としては以下のように記述しました。
    
    ```java
    @OpenAPIDefinition(info = @Info(
        title = "受講生管理システム",
        description = "受講生情報を管理するためのシステムです。主に受講生の個人情報と受講しているコース情報の管理、操作を行います"
    
    ))
    @SpringBootApplication
    public class StudentManagementApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(StudentManagementApplication.class, args);
      }
    
    }
    ```
    
    - `@OpneAPIDefinition`内が該当箇所です
- ここに記述した内容がAPI仕様書の画面上部に表示されます
- 今回は最低限の記述のみ行いましたが、サーバー情報、License情報、連絡先情報などもろもろ記述することも可能なようです
    - [公式ドキュメント-OpenAPIDefinitionの詳細](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations#openapi-annotations)

### Controllerへの記載

#### シンプルなGETメソッドの場合

- 引数なし、例外処理なしの場合の記述が以下です。
    
    ```java
      @Operation(
          summary = "受講生詳細一覧の検索",
          description = "受講生詳細の一覧を検索します。全件検索を行うので、条件指定は行いません。",
          responses = {@ApiResponse(
              content = @Content(mediaType = "application/json",
                  array = @ArraySchema(schema = @Schema(implementation = StudentDetail.class))
              )
          )}
      )
      @GetMapping("/studentList")
      public List<StudentDetail> getStudentList() {
        return service.getAllStudentDetailList();
      }
    ```
    
    - メソッドの前に`@Operation`をつけます
    - `@Operation`のプロパティとして以下を設定しています
        - `summary`
            - このメソッドが何であるかの要約
            - 文字列を直接指定
        - `description`
            - このメソッドがなんであるかの説明
            - 文字列を直接指定
        - `responses`
            - レスポンスとして何が返ってくるかの説明
            - `@ApiResponse`をネストして使う
    - `@ApiResponse`のプロパティとして以下を設定しています
        - `content`
            - 返り値の詳細を記述
            - `@Content`をネストして使う
    - `@Content`のプロパティとして以下を設定しています
        - `mediaType`
            - 返り値のContentType
            - REST APIの場合、application/jsonになることが多いと思います
        - `array `
            - 返り値が配列であることを明示
            - 配列の中身を示すために`@ArraySchema`をネストして使用
    - `@ArraySchema`のプロパティとして以下を設定しています
        - `schema`
            - 配列の中身のデータ構造を指定
            - `@Schema`をネストして使用
    - `@Schema`のプロパティとして以下を設定しています
        - `implementation`
            - データモデルを指定します
            

#### パスで引数を受け取るGETメソッドの場合

- パスで引数を受け取る、エラー設定が複数ある場合のGETメソッドの記述です。
    
    ```java
      @Operation(
          summary = "受講生検索",
          description = "パスで指定されたIDに該当する受講生を検索します。IDに紐づく受講生が存在しない場合エラーを発生させます。",
          responses = {
              @ApiResponse(
                  responseCode = "200", description = "ok",
                  content = @Content(
                      mediaType = "application/json",
                      schema = @Schema(implementation = StudentDetail.class)
                  )),
              @ApiResponse(
                  responseCode = "404", description = "指定されたIDの受講生が存在しない場合のエラー",
                  content = @Content(
                      mediaType = "application/json",
                      schema = @Schema(implementation = ErrorResponse.class)
                  )
              ),
              @ApiResponse(
                  responseCode = "400", description = "UUIDの形式が誤っていた際のバリデーションエラー",
                  content = @Content(
                      mediaType = "application/json",
                      schema = @Schema(implementation = ErrorResponse.class)
                  )
              )
          },
          parameters = {
              @Parameter(in = ParameterIn.PATH, name = "id",
                  required = true,
                  description = "受講生ID",
                  schema = @Schema(
                      type = "string",
                      format = "uuid",
                      description = "自動生成されたUUID",
                      example = "5998fd5d-a2cd-11ef-b71f-6845f15f510c"
                  )
              )}
      )
      @GetMapping("/student/{id}")
      public StudentDetail getStudent(@PathVariable @Pattern(
          regexp = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$")
      String id)
          throws StudentNotFoundException {
        return service.getStudentDetailById(id);
      }
    ```
    
- 未解説箇所のみ追記します
    - `@ApiResponse`のプロパティとして以下を追記しています
        - `responseCode`
            - HTTPステータスコードを文字列で指定します
            - 200は記載しなくても問題ないですが、記載しない場合仕様書上で”default”と記述されます
        - `description`
            - `responseCode`と併記する場合が多いようです
            - どのような意味合いがあるステータスなのかを記述します
        - `schema`
            - 解説済みですが、補足です
            - エラーメッセージはクラスとして定義しておくと、使いまわしやすく、API仕様書でも反映させやすいのでやっておくとよいです
    - `@Operation` のプロパティとして以下を追記しています
        - `parameters`
            - 引数がなんであるかの記述をします
            - `@Parameter` をネストして使用します
    - `@Parameter`　のプロパティとして以下を記述しています
        - `in`
            - どのように引数を渡すかを記述します
            - 今回はPathに含まれるので、PATHを指定しています
        - `name`
            - 引数の名前を記述します
            - ここで記述した引数名が仕様書上でもそのまま使われます
        - `required`
            - 必須かどうかを記述します
            - `true`の場合、必須であることが仕様書に明記されます
        - `schema`
            - `@ApiResponse`で使ったschemaプロパティと同様です
    - `@Shema` の補足
        - データモデルで定義していないデータ構造を返したい場合、`
        
        `implementation``が使えないため、手動で記述する必要があります
            - `type'
                - データ型
            - `format`
                - 固有のフォーマットがあれば記述
            - ‘description’
                - データ構造の説明
            - `example`
                - 具体例、記述しておくと仕様書にも反映されるのでわかりやすくなる

#### 例外処理あり、リクエストボディを受け取るPOSTメソッド

- リクエストボディとして引数を受け取る、例外処理があるPOSTメソッドの記述です
    
    ```java
      @Operation(
          summary = "受講生登録",
          description = "受講生を登録します",
          responses = {
              @ApiResponse(
                  responseCode = "200", description = "ok",
                  content = @Content(
                      mediaType = "application/json",
                      schema = @Schema(implementation = StudentDetail.class)
                  )
              ),
              @ApiResponse(
                  responseCode = "400", description = "リクエストボディのバリデーションエラー",
                  content = @Content(
                      mediaType = "application/json",
                      schema = @Schema(implementation = ErrorResponse.class)
                  )
              )
          },
          requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
              description = "新規登録したい受講生詳細　※受講生のid,remark,isDeleted、受講生コース情報のid,studentId,startDate,endDateは自動付与されるためリクエストボディに含まれません",
              required = true,
              content = @Content(
                  schema = @Schema(implementation = StudentDetail.class)
              )
          )
      )
      @PostMapping("/registerStudent")
      public ResponseEntity<StudentDetail> registerStudent(
          @RequestBody @Valid StudentDetail studentDetail) {
        StudentDetail registeredStudentDetail = service.registerStudent(studentDetail);
        return ResponseEntity.ok(registeredStudentDetail);
      }
    ```
    
    - 未解説の箇所のみ追記します
        - `@Operation`に以下を追記しています
            - `requestBody`
                - `@RequestBody`をネストして使います
                - `@RequestBody`アノテーションはSpring Boot標準のものもあるので、自身の環境では記載にあるようなメソッドチェーンで記述されました
        - `@RequestBody`のプロパティとして以下を記述しています
            - `description`
                - どのようなリクエストボディなのかの説明
            - `required`
                - 必須かどうかの設定
            - `content`
                - `@Operation`のcontentと同様

### データモデルへの記載

データモデルへの記述は比較的わかりやすいので、簡単な解説に留めます

```java
@Schema(description = "受講生")
public class Student {

  @Schema(description = "ID、UUIDを自動付与", example = "5998fd5d-a2cd-11ef-b71f-6845f15f510c")
  private String id;

  @Schema(description = "氏名", example = "山田 太郎")
  @NotBlank(message = "入力が必要です")
  private String fullName;

  @Schema(description = "フリガナ、カタカナと半角・全角スペースのみ許可", example = "ヤマダ　タロウ")
  @Pattern(
      regexp = "^[ァ-ヶー\\s　]+$",
      message = "カタカナとスペースのみを入力してください"
  )
  private String kana;

  @Schema(description = "ニックネーム", example = "たろ")
  private String nickName;

  @Schema(description = "メールアドレス", example = "yamada@example.com")
  @NotBlank(message = "入力が必要です")
  @Email(message = "メールアドレスの形式が誤っています")
  private String email;

  @Schema(description = "居住地域、都道府県＋市区町村までを想定", example = "東京都港区")
  @NotBlank(message = "入力が必要です")
  private String city;

  @Schema(description = "年齢、0~150までを許可", example = "32")
  @Range(min = 0, max = 150, message = "正しい値を入力してください")
  private int age;

  @Schema(description = "性別", example = "Male")
  private Gender gender;

  @Schema(description = "備考", example = "入院のため利用休止中")
  private String remark;

  @Schema(description = "削除フラグ", example = "false")
  private Boolean isDeleted;
```

- クラス名の上の`@Schema`
    - このデータモデルの説明を記述できます
- 各フィールド上の`@Schema`
    - 各フィールドの説明を記述できます
    - `example`を記述すると、仕様書上の各メソッドの引数や返り値の箇所でも反映され、わかりやすくなります

## おわりに

初見で記述を始めた際には各アノテーションが持てるプロパティがわからず、記述も億劫でしたが、噛み砕いて理解すると比較的シンプルで慣れれば時間をそこまでかけずとも記述可能だと感じました。

どなたかのご参考になりますと幸いです。

## 参考

- [公式ドキュメント（GitHub）](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations#media-annotations))
- [【Java】SwaggerでAPI仕様を見れるところまでやってみた（swagger-core × springdoc-openapi-ui）](https://qiita.com/crml1206/items/e47ec484af750d301953)