OpenAPI使い方メモ

# OpenAPI とは
- HTTP の Web API の仕様を記述するための言語
- 特定のプログラミング言語に依存しないので、様々な言語での Web API の仕様を定義するのに利用できる
- YAML または JSON で記述できる
    - ここでは YAML 前提で整理する
- 作成した OpenAPI の定義ファイル(YAML or JSONファイル)を元に、いろいろなものを自動生成できる
    - API仕様書
    - クライアントやサーバーのソースコード
        - 様々な言語・ライブラリで出力できる
    - モックサーバー

## Swagger との関係
- 元は Swagger (スワッガー)という名前で開発されていたが、2015年に仕様部分が OpenAPI として独立したプロジェクトになった
- 現在の Swagger はドキュメントやソースコードなどの自動生成ツールとして提供されている

# Hello World
## 環境構築
- とりあえず簡単に試せる環境を整える
- VS Code の拡張に[OpenAPI (Swagger) Editor](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi)というのがあるので、これをインストールする

## ファイル作成
- VS Code のパレットを開いて「Create new OpenAPI v3.1 file (YAML)」を選択する
- 適当な名前で保存する(なんかYAMLファイルの拡張子は`.yml`が一般的らしい。知らんけど)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/7c9c1397-6051-46a8-b332-912a58c2d731.png)

- サンプルのファイルが作成されるので、エディタの右上にあるプレビューボタンを押す

![gomi.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/29d8ec92-b367-4311-b7ba-376ecf106304.jpeg)


- 生成されたドキュメントが確認できる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/0f7ba6b2-cc4e-4fca-b443-df6dab4ad4d4.png)

# ざっくり一巡り
細かい書き方・使い方について入る前に、ざっくりとOpenAPIを用いた基本的な仕様の書き方について確認していく。

## 基本構造
```yaml
# OpenAPI のバージョン
openapi: '3.1.1'
info:
  # このAPIドキュメントのタイトル
  title: API Title
  # このAPIドキュメントのバージョン
  version: '1.0'
paths:
  # 各APIの定義
  /test:
    get:
      responses:
        '200':
          description: OK
```

- 最低限必要な項目は以下
    - `openapi`
        - OpenAPI のバージョンを記述する
    - `info`
        - `title`
            - APIのタイトル
        - `version`
            - このAPIドキュメントのバージョン
            - OpenAPI のバージョンとは異なる
- `paths` の下に各APIの定義を記述していく

## パスの定義
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  # paths の下に、パスごとにAPI定義を並べる（パスは必ずスラッシュで始める）
  /hoge:
    description: hoge
  /fuga:
    description: fuga
  /piyo:
    description: piyo
```

- `paths` の下にはパスをキーにして各パスの定義である [Path Item Object](https://spec.openapis.org/oas/latest.html#path-item-object) を並べる
- キーであるパスは、必ずスラッシュ (`/`) から始める形で記述する

## オペレーションの定義
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    # HTTPメソッドをキーにして各オペレーションを定義する
    get:
      # GET メソッドのオペレーション定義
      description: get hoge
    post:
      # POST メソッドのオペレーション定義
      description: post hoge
  /fuga:
    delete:
      description: delete fuga
  /piyo:
    put:
      description: put piyo
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/cba168ff-a5d7-4315-a934-7580a95bb960.png)

- パスの下には、そのパスで使用できるオペレーションの定義（[Operation Object](https://spec.openapis.org/oas/latest.html#operation-object)）をHTTPメソッドごとに並べる
    - キーには HTTP メソッドを指定する

## レスポンスの定義
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    get:
      # ステータスコードごとにレスポンスを定義する
      responses:
        # ステータスコードは文字列で定義する
        "200":
          # 200 OK の場合
          description: 200 response
        "400":
          # 400 Bad Request の場合
          description: 400 response
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/318abd58-9d3a-4abe-a8da-071830d37a6b.png)

- 各 Operation Object の `responses` で、レスポンスを定義できる
- レスポンスの定義は、HTTPのステータスコードごとに [Response Object](https://spec.openapis.org/oas/latest.html#response-object) で記述する
    - Response Object は `description` が必須
    - ステータスコードは文字列で定義しなければならない
        - YAML と JSON との間で互換性を保つため、ってドキュメントに書いてある
        - > This field MUST be enclosed in quotation marks (for example, “200”) for compatibility between JSON and YAML.
            - [4.8.16.2 Patterned Fields](https://spec.openapis.org/oas/latest.html#patterned-fields-0)


### コンテンツ定義
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    get:
      responses:
        "200":
          description: success
          # レスポンスボディの内容を、コンテンツタイプごとに定義する
          content:
            application/json:
              # applicaiton/json の場合
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
            text/xml:
              # text/xml の場合
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                xml:
                  name: "Hoge"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/baafaa6e-ead1-44f8-bc47-b6b8512554b6.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/e87e1970-67a7-4226-ba50-bbc97d3bfbaf.png)

- 各 Response Object の `content` で、レスポンスのコンテンツを定義できる
- コンテンツの定義は [Media Type Object](https://spec.openapis.org/oas/latest.html#media-type-object) でコンテンツタイプごとに定義する
- 各コンテンツでは、 `schema` で具体的なレスポンスコンテンツの構造を定義できる
    - この `schema` の記述は、[JSON Schema Specification Draft 2020-12](https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-01) で定義されたものをベースとしている
        - 一部 OpenAPI 用に拡張したりしているっぽい

## レスポンスヘッダーの定義
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    get:
      responses:
        "200":
          description: success
          # レスポンスヘッダーの定義
          headers:
            "X-Hoge":
              schema:
                type: string
            "X-Fuga":
              schema:
                type: integer
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/379dd33d-d1f6-4188-affb-5fdbee97f5f5.png)
 
- [Response Object](https://spec.openapis.org/oas/latest.html#response-object) の `headers` で、そのレスポンスで返すヘッダーを定義できる
- `headers` は Map で指定し、キーにヘッダー名、値に [Header Object](https://spec.openapis.org/oas/latest.html#header-object) を使用する


## リクエストの定義
オペレーションのリクエストには、次の二種類の入力データの定義が用意されている

- パラメータ
- リクエストボディ（メッセージペイロード）

### パラメータの定義
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  # パスパラメータは {} で囲うことで定義できる
  /hoge/{foo}:
    get:
      parameters:
        - name: foo
          # パラメータがどこから渡されるかを in で指定する
          in: path
          required: true
          schema:
            type: string
        - name: bar
          in: query
          schema:
            type: string
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/23d08393-53a6-4500-b4f4-5ba3e07684b9.png)

- パラメータは、各 Operation Object の `parameters` で [Parameter Object](https://spec.openapis.org/oas/latest.html#parameter-object) の配列で定義する
- `name` には、パラメータの名前を指定する
- `in` には、そのパラメータがどこから入力されるかを指定する
    - `path`, `query`, `header`, `cookie` のいずれかを指定する
    - `path` はパスパラメータを指す
        - この場合、パス定義の中のテンプレート（波括弧(`{}`)で囲った部分）と `name` が一致している必要がある
    - `query` はクエリパラメータを指す
- `required` は、そのパラメータが必須かどうかを boolean で指定する
    - `true` を設定した場合、そのパラメータは必須パラメータとなる
    - 基本的に指定は任意
    - ただし、 `in` に `path` を指定している場合は `required: true` を必ず設定しなければならない
- `schema` は必ず指定しなければならない
- Parameter Object では、 `name` と `in` の指定は必須となっている

### リクエストボディの定義
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge/{foo}:
    get:
      # リクエストボディの定義
      requestBody:
        content:
          # コンテンツタイプごとにボディの定義を記述できる
          application/json:
            schema:
              type: integer
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/9a9b1ad0-d77f-458e-b7c1-4c5f2369d626.png)

- リクエストボディ(メッセージペイロード)は、 Operation Object の `requestBody` で [Request Body Object](https://spec.openapis.org/oas/latest.html#request-body-object) を用いて定義する
- `content` が必須となっている
- `content` の書き方は、レスポンスのコンテンツと同じ

## コンポーネントの定義
- `schema` などは、同じ定義を複数箇所で使いまわししたくなることがよくある
- そういった場合は、コンポーネントという形で定義しておき、それを各場所から参照する形で利用できる

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    get:
      requestBody:
        content:
          application/json:
            schema:
              # components で定義している内容を JSON Pointer で参照できる
              $ref: "#/components/schemas/foo"
# 共通の定義を components でまとめておける
components:
  schemas:
    foo:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/cfd4de80-9f76-46e1-b1f4-113a471e32b0.png)

- コンポーネントはルートの `components` で、 [Components Object](https://spec.openapis.org/oas/v3.1.0#components-object) を使って定義する
- Components Object には `schemas` や `responses`, `parameters` など各所で再利用するためのコンポーネントを定義するプロパティが用意されている
- 定義したコンポーネントを参照する際は、 `$ref` を使用する
    - 値には参照先のコンポーネントの URI を指定する
    - 同じファイル内のコンポーネントを参照する場合は、 `#` 始まりでコンポーネントのパスを記述する
        - これは [RFC6901 の JSON Pointer](https://tex2e.github.io/rfc-translater/html/rfc6901.html) で定義された仕様に従っている
    - 別のファイルのコンポーネントを参照する場合は `./filepath.yml#/components/...` のように書くこともできる

## 例の定義
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      parameters:
        - name: foo
          in: query
          schema:
            type: string
          # パラメータに実際どういう値が渡せるか、例を記載できる
          example: hello world
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/1af61a05-2f0c-4c3f-a1d7-9cfddaf96416.png)

- `example` に値の例を記載できる
- `example` に記載する値の型は `schema` で定義した型と一致している必要がある
- `example` は Parameter Object, Media Type Object, Schema Object で使用できる

### 複数の例を定義する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: integer
                name:
                  type: string
            # 複数の例（正常系や異常系など任意）を定義できる
            examples:
              normalPattern:
                # 正常パターン
                value:
                  id: 111
                  name: Hello
              illegalPattern:
                # 異常パターン
                value:
                  id: 999
                  name: World
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/3007cc42-b7d7-4829-a641-a97d7963e81c.png)

- `examples` を使うと複数の例を定義できる
- `examples` は Map 型で、キーにパターンを識別する名前、値に [Example Object](https://spec.openapis.org/oas/latest.html#example-object) を指定する
- `examples` は Parameter Object や Media Type Object で使用できる
- `example` と `examples` は排他の関係で、片方のみ定義できる（両方同時に定義することはできない）

## APIサーバーを定義してAPIを実際に試す
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
# try it でリクエストを送信する先を複数定義して切り替えができる
servers:
  - url: http://localhost:8080/api
    description: ローカル開発環境
  - url: http://staging/api
    description: ステージング環境
paths:
  /hoge:
    get:
      description: get hoge
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/893e5923-4b8e-4163-80dc-dcb6604c55e1.png)

- `servers` で、APIを実際に試すときのアクセス先を定義できる
- `servers` は [Server Object](https://spec.openapis.org/oas/latest.html#server-object) の配列で指定する
- `url` で、APIにアクセスするときのベースとなる URL を指定する
    - この URL に `paths` で定義された個々のパスが接続された形でリクエストが実行される
- APIの実行は個々のオペレーションにある [Try it out] ボタンから試せる


# JSON Schema

- 各パラメータの型やリクエスト・レスポンスボディの定義は JSON Schema (Draft 2020-12)の仕様に則って記述する
- ここでは、 [Schema Object](https://spec.openapis.org/oas/latest.html#schema-object) = JSON Schema の書き方について確認する
- ただし、 OpenAPI で使用する定義は JSON Schema と完全に互換性があるわけではなく、一部独自の拡張を入れているものもあるので注意

## 数値
### 整数
```yaml
schema:
  type: integer
```

```json:OK
1
1.0
-1
0
```

```json:NG
1.1
0.1
```

- `integer` を指定すると、整数値であることを定義できる
- `1.0` のように少数部が `0` の場合は整数として扱われる

### 実数
```yaml
schema:
  type: number
```

```json:OK
1.1
-1.2
10
1.2e3
```

```json:NG
"20"
```

- `number` を指定すると、少数を含んだ数値であることを定義できる
- 指数表記も可能

### 倍数
```yaml
schema:
  type: number
  multipleOf: 5
```

```json:OK
0
5
15
-10
```

```json:NG
6
12
```

- `multipleOf` を使用すると、指定した値の倍数であることを定義できる

### 範囲（閉区間）

```yaml
schema:
  type: integer
  minimum: 2
  maximum: 5
```

```json:OK
2
3
5
```

```json:NG
1
6
```

- `minimum` で指定した値以上、 `maximum` で指定した値以下を定義できる

### 範囲（開区間）

```yaml
schema:
  type: integer
  exclusiveMinimum: 2
  exclusiveMaximum: 5
```

```json:OK
3
4
```

```json:NG
2
5
```

- `exclusiveMinimum` で指定した値より大きい、 `exclusiveMaximum` で指定した値より小さいことを定義できる

## 真偽値
```yaml
schema:
  type: boolean
```

```json:OK
true
false
```

```json:NG
"true"
0
null
[]
```

- `boolean` を指定すると、真偽値であることを定義できる
- `0` や `null` のような、 JavaScript だと `false` 扱いになるような値は設定できない

## null値
```yaml
schema:
  type: "null"
```

- リテラルの `null` ではなく、文字列で `"null"` と指定している点に注意

```json:OK
null
```

```json:NG
"null"
""
false
[]
0
```

- `"null"` を指定すると、 `null` 値であることを定義できる

## 文字列
```yaml
schema:
  type: string
```

```json:OK
"foo"
"bar"
""
```

```json:NG
0
false
["array"]
{"type": "object"}
null
```

- `type` に `string` を指定すると、文字列であることを定義できる

### 文字数を定義する
```yaml
schema:
  type: string
  minLength: 2
  maxLength: 4
```

```json:OK
"ab"
"abcd"
"あいうえ"
"𠮷abc"
```

```json:NG
"a"
"abcde"
"あいうえお"
```

- `minLength`, `maxLength` を使用すると、文字数の最小と最大を定義できる
- 文字数はUnicodeのコードポイントの数でカウントするので、漢字やサロゲートペア文字も１文字１つとしてカウントされる

> The minLength keyword restricts string instances to consists of an inclusive minimum **number of Unicode code-points (logical characters)**, which is not necessarily the same as the number of bytes in the string.
> https://www.learnjsonschema.com/2020-12/validation/minlength/

### 正規表現

```yaml
schema:
  type: string
  pattern: ^\d{3}-[a-z]+$
```

- 数値が３桁、ハイフンを挟んでアルファベット小文字が１つ以上続くという正規表現

```json:OK
"012-foobar"
"987-test"
```

```json:NG
"12-foo"
"012-123"
```

- `pattern` で文字列の内容を正規表現で制限できる
- 正規表現の構文は [ECMA-262](https://ecma-international.org/publications-and-standards/standards/ecma-262/) に準拠している
- 詳細な構文については以下を参照
    - [JSON Schema - Regular Expressions](https://json-schema.org/understanding-json-schema/reference/regular_expressions)

## 定数

```yaml
schema:
  type: string
  const: "foo"
```

```json:OK
"foo"
```

```json:NG
"bar"
10
```

- `const` を使用すると、特定の値のみを許可するように定義できる

## デフォルト値

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: string
              # デフォルト値を定義
              default: "foo"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/1a7d3076-c60c-484f-8e86-375688e59c75.png)

- `default` を使用すると、値が指定されなかった場合のデフォルト値を表現できる
- ただし、これはドキュメントなどを生成したときに「デフォルト値があるよ」ということを表現するためのもので、スキーマ検証の際にデフォルト値があることを考慮して値が無くても**補完するためのものではない**


## 列挙型
```yaml
# フロースタイルで定義した場合の例
schema:
  type: string
  enum: ["aaa", "bbb", "ccc"]

# ブロックスタイルで定義した場合の例
schema:
  type: integer
  enum:
    - "aaa"
    - "bbb"
    - "ccc"
```

```json:OK
"aaa"
"bbb"
"ccc"
null
```

```json:NG
"AAA"
```

- `enum` を使うと、その項目に設定可能な値を列挙できる
- `null` は OK になる

### 列挙型を Component で定義して参照する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
servers:
  - url: http://localhost:8080/echo
paths:
  /hoge:
    get:
      parameters:
        - name: foo
          in: query
          schema:
            # enum の定義を参照して利用する
            $ref: "#/components/schemas/fooEnumType"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: integer
                bar:
                  # enum の定義を参照して利用する
                  $ref: "#/components/schemas/fooEnumType"
components:
  schemas:
    # enum の定義
    fooEnumType:
      type: string
      enum:
        - "aaa"
        - "bbb"
        - "ccc"
```

- `components` の `schemas` であらかじめ列挙型を定義しておいて、各項目で参照するという使い方ができる

## オブジェクト
```yaml
schema:
  type: object
```

```json:OK
{"foo": "FOO"}
{"fizz": "FIZZ", "buzz": "BUZZ"}
```

```json:NG
1
true
null
"abc"
[1, 2, 3]
```

- `object` を指定すると、オブジェクトであることを定義できる
- これだけだと、オブジェクトの中身は何でもいいことになる

### プロパティを定義する
```yaml
schema:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
```

```json:OK
{
    "id": 10,
    "name": "hoge"
}
{
    "id": 20,
    "name": "fuga",
    "age": 21
}
{
    "id": 30
}
```

```json:NG
{
    "id": "10"
}
```

- `properties` を使用すると、オブジェクトのプロパティのスキーマを定義できる
- `properties` は Map で指定する
    - キーはプロパティの名前
    - 値は、そのプロパティのスキーマ定義(Schema Object)
- `properties` だけの場合、プロパティが不足していたり余計なプロパティがあっても invalid にはならない

### 必須プロパティを定義する
```yaml
schema:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
  required: ["id"]
```

```json:OK
{
    "id": 10,
    "name": "foo"
}
{
    "id": 20
}
{
    "id": 30,
    "age": 18
}
```

```json:NG
{
    "name": "bar"
}
```

- `required` で必須のプロパティを定義できる
- 配列で指定し、要素には必須とするプロパティの名前を設定する

### 未定義のプロパティを制限する
```yaml
schema:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
  additionalProperties: false
```

```json:OK
{
    "id": 10,
    "name": "foo"
}
{
    "id": 20
}
```

```json:NG
{
    "id": 30,
    "name": "bar",
    "age": 20
}
```

- `additionalProperties` に `false` を設定すると、定義外のプロパティを設定できないように制限できる

### 未定義のプロパティのスキーマを定義する
```yaml
schema:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
  additionalProperties:
    type: boolean
```

```json:OK
{
    "id": 10,
    "name": "foo"
}
{
    "id": 20,
    "other": false
}
```

```json:NG
{
    "id": 30,
    "other": "test"
}
```

- `additionalProperties` に Schema Object を指定すると、未定義のプロパティのスキーマを定義できる

### プロパティ名を正規表現で定義する
```yaml
schema:
  type: object
  patternProperties:
    ^S_:
      type: string
    ^I_:
      type: integer
```

```json:OK
{
    "S_1": "foo",
    "I_1": 10
}
{
    "S_21": "bar",
    "I_19": 6
}
{
    "another": "any"
}
```

```json:NG
{
    "S_20": 21
}
{
    "I_17": false
}
```

- `patternProperties` を使用すると、プロパティの名前を正規表現で定義できる
- 正規表現がマッチしたプロパティのみが、指定したスキーマ定義と一致しなければならない

### プロパティ名のスキーマを定義する
```yaml
schema:
  type: object
  propertyNames:
    pattern: ^[A-Z][a-zA-Z]+$
    minLength: 2
    maxLength: 5
```

- プロパティ名はアルファベットのみで先頭は大文字、さらに2文字以上5文字以下であるというスキーマ定義

```json:OK
{
    "Id": 1,
    "Name": "foo"
}
```

```json:NG
{
    "Weight": 65.4
}
{
    "id": 2,
    "name": "bar"
}
{
    "I": 3
}
```

- `propertNames` を使用すると、プロパティ名のスキーマを定義できる
- プロパティ名は文字列である必要があるので、少なくとも `type: string` は設定されている前提となる

### プロパティの数を制限する
```yaml
schema:
  type: object
  minProperties: 2
  maxProperties: 5
```

```json:OK
{
    "id": 1,
    "name": "foo"
}
{
    "id": 2,
    "name": "bar",
    "age": 14,
    "tall": 165.2,
    "weight": 62.3
}
```

```json:NG
{
    "id": 3
}
{
    "id": 2,
    "name": "fizz",
    "age": 14,
    "tall": 165.2,
    "weight": 62.3,
    "country": "jp"
}
```

- `minProperties` でプロパティ数の最小数を、 `maxProperties` で最大数を定義できる

### 条件付き必須
```yaml
schema:
  type: object
  dependentRequired:
    hoge: ["fuga"]
```

```json:OK
{
    "id": 1
}
{
    "fuga": 10
}
{
    "hoge": "HOGE",
    "fuga": "FUGA"
}
```

```json:NG
{
    "hoge": "HOGE"
}
```

- `dependentRequired` を使用すると、あるプロパティが存在するときだけ別のプロパティを必須にする、という制御ができる
- `dependentRequired` は Map で指定する
    - キーに指定したプロパティが存在した場合に、値に配列で指定した名前のプロパティたちが必須になる
    - `foo: ["fizz", "buzz"]` と指定すれば、 `foo` というプロパティが存在したときに `fizz`, `buzz` というプロパティが必須になる

## If-Then-Else

```yaml
schema:
  if:
    type: string
  then:
    pattern: ^[a-z]+$
  else:
    type: integer
```

```json:OK
"hoge"
10
```

```json:NG
"012"
true
```

- `if`, `then`, `else` を用いると、条件が満たされたときだけ有効になるスキーマを定義できる
- 検証対象の値が `if` で定義したスキーマ定義を満たす場合は、 `then` で指定したスキーマ定義も満たすかどうかが検証される
- 検証対象の値が `if` で定義したスキーマを満たさない場合は、 `else` で指定したスキーマ定義を満たすかどうかが検証される
- `else` と `then` は、不要であればいずれかを省略することも可能
- 「オブジェクトのプロパティが特定の値だった場合は」みたいなときは、以下のように `const` を利用する

```yaml
schema:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
  if:
    properties:
      name:
        const: "foo"
  then:
    properties:
      age:
        type: integer
    required: ["age"]
```

- `name` の値が `"foo"` の場合のみ、 `age` という整数値のプロパティを必須で定義している

```json:OK
{
    "id": 10,
    "name": "hoge"
}
{
    "id": 20,
    "name": "foo",
    "age": 12
}
{
    "id": 50,
    "name": "bar",
    "age": true
}
```

```json:NG
{
    "id": 30,
    "name": "foo"
}
{
    "id": 40,
    "name": "foo",
    "age": "20"
}
```

## 配列
```yaml
schema:
  type: array
```

```json:OK
[1, 2, 3, 4, 5]
["a", "b", "c", "d"]
[1, "a", {"foo": "Foo"}]
```

```json:NG
{"Not": "an array"}
```

- `type` に `array` を指定すると、配列を表す
- これだけの場合、配列の中身は何でもいい（数値でも文字列でもオブジェクトでも、なんでもアリ）

### 要素の型を限定する
```yaml
schema:
  type: array
  items:
    type: number
```

```json:OK
[1, 2, 3, 4, 5]
[]
```

```json:NG
[1, 2, "3", 4, 5]
```

- `items` で要素の型を限定できる
- 指定された型以外の要素があるとNG
- 空配列はOK

### 先頭から任意の数の要素の型を限定する
```yaml
schema:
  type: array
  prefixItems:
    - type: number
    - type: string
    - enum: ["foo", "bar"]
    - enum: ["fizz", "buzz"]
```

```json:OK
[1000, "Hoge", "foo", "fizz"]
[1200, "Fuga", "bar"]
[1300, "Foo", "foo", "bar", "extra"]
```

```json:NG
[1000, "Hoge", "fizz", "foo"]
["str", "Hoge", "foo", "fizz"]
```

- `prefixItems` を使用すると、先頭から任意の数の要素の型を限定できる
- ここでは先頭4つの要素の型を、それぞれ以下のように定義している
    - １つ目は数値のみ
    - ２つ目は文字列のみ
    - ３つ目は enum で "foo" または "bar" のみ
    - ４つ目は enum で "fizz" または "buzz" のみ
- 要素が不足する場合はOK
- `prefixItems` で定義した数より後ろの要素は何が来てもOK

#### prefixItems で定義した要素以外の要素は入れられないようにする
```yaml
schema:
  type: array
  prefixItems:
    - type: number
    - type: string
  items: false
```

```json:OK
[100, "text"]
[100]
```

```json:NG
[100, "text", "foo"]
```

- `prefixItems` の指定に加えて `items` に `false` を設定すると、 `prefixItems` で定義した要素より後ろには要素を入れられなくなる

#### prefixItems で定義した要素以外の要素を限定する
```yaml
schema:
  type: array
  prefixItems:
    - type: number
    - type: string
  items:
    type: number
```

```json:OK
[100, "text"]
[100, "text", 1]
```

```json:NG
[100, "text", "foo"]
```

- `prefixItems` に加えて `items` で要素の型を定義すると、 `prefixItems` より後ろの要素は `items` で指定された型の値しか入れられなくなる

### 最低１つ含まれる要素を定義する
```yaml
schema:
  type: array
  contains:
    type: number
```

```json:OK
[1, 2, 3]
[1, "foo", false]
```

```json:NG
["foo", "bar"]
[true, false]
[]
```

- `contains` を使用すると、指定された条件を満たす要素が最低１つは存在していることを検証できる
- 上記例では、型が `number` である要素が最低１つ入っていることを定義している

#### 最低/最大 n 個含まれる要素を定義する
```yaml
schema:
  type: array
  contains:
    type: number
  minContains: 2
  maxContains: 4
```

```json:OK
[1, "foo", 2, "bar"]
[1, false, 2, 3, 4, "fizz"]
```

```json:NG
[1, "foo", "bar"]
[1, 2, false, 3, 4, true, 5]
```

- `contains` と合わせて `minContains`, `maxContains` を使用することで、要素数の最小数を最大数を定義できる

### 要素数を定義する

```yaml
schema:
  type: array
  minItems: 1
  maxItems: 3
```

```json:OK
[1]
["hoge", "fuga", "piyo"]
```

```json:NG
[]
[1, 2, "foo", "bar"]
```

- `minItems`, `maxItems` で、要素数の最小と最大を定義できる

### 要素に重複がないことを定義する

```yaml
schema:
  type: array
  uniqueItems: true
```

```json:OK
[1, 2, 3, 4]
[
  {
    "age": 12,
    "name": "Hoge"
  },
  {
    "age": 15,
    "name": "Fuga"
  },
  {
    "age": 13,
    "name": "Piyo"
  }
]
[]
```

```json:NG
[1, 2, 3, 1]
[
  {
    "age": 12,
    "name": "Hoge"
  },
  {
    "age": 15,
    "name": "Fuga"
  },
  {
    "name": "Hoge",
    "age": 12
  }
]
["foo", "bar", "fizz", "foo"]
```

- `uniqueItems` に `true` を設定すると、全ての要素がユニーク（重複が許されない）ことを定義できる

## スキーマ定義を組み合わせる
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    get:
      parameters:
        - name: foo
          in: query
          schema:
            # oneOf の例
            oneOf:
              - type: string
              - type: integer
      requestBody:
        content:
          application/json:
            schema:
              # anyOf の例
              anyOf:
                - type: object
                  properties:
                    value:
                      type: string
                - type: object
                  properties:
                    val:
                      type: string
      responses:
        "200":
          description: 200 ok
          content:
            application/json:
              schema:
                # allOf の例
                allOf:
                  - type: object
                    properties:
                      id:
                        type: integer
                  - type: object
                    properties:
                      name:
                        type: string
```

![localhost_8080_swagger_ (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/b684b035-9cd7-4002-94b5-653ed003b92c.png)

- [Schema Object](https://spec.openapis.org/oas/latest.html#schema-object) では、 `oneOf`, `anyOf`, `allOf` を使って複数のスキーマ定義を混ぜることができる
    - これ自体は JSON Schema で定義された仕様
- それぞれの値は、 Schema Object の配列で指定する
- `oneOf` は、スキーマ定義のうち1つだけにマッチしなければならない
    - 2つ以上にマッチしてはいけない
- `anyOf` は、スキーマ定義のうちいずれか１つ以上にマッチすればいい
    - 複数にマッチしてもいい
    - どれともマッチしないのはダメ
- `allOf` は、全てのスキーマ定義にマッチしなければならない

### 継承を表現する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    get:
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChildType"
components:
  schemas:
    # 親のスキーマ定義
    ParentType:
      type: object
      properties:
        childType:
          type: string
    # 子のスキーマ定義
    ChildType:
      allOf:
        # 親のスキーマ定義を allOf で取り込む
        - $ref: "#/components/schemas/ParentType"
        # 子固有のスキーマを追加で定義する
        - type: object
          properties:
            childValue:
              type: string
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/bbbe8c00-25e6-4269-a9a0-c2853e6c4730.png)

- `allOf` を用いることで Schema Object の継承が表現できる
- `allOf` の１つに親の Schema Object を含めることで、親のスキーマ定義をすべて持つスキーマ、すなわち継承したスキーマを定義できる

### ポリモーフィズムを表現する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      requestBody:
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/Foo"
                - $ref: "#/components/schemas/Bar"
      responses:
        "200":
          description: 200 OK
components:
  schemas:
    # 親スキーマ定義
    ParentType:
      type: object
      properties:
        childType:
          type: string
      # サブタイプの識別プロパティは必須にしておく
      required: ["childType"]
      discriminator:
        # サブタイプを決定するための値が格納されたプロパティ名を指定する
        propertyName: childType
        # propertyName で指定したプロパティの値とサブタイプのスキーマとのマッピングを定義する
        mapping:
          # childType の値が foo なら Foo スキーマ
          foo: Foo
          # childType の値が bar なら Bar スキーマ
          bar: Bar
    # Foo スキーマ定義
    Foo:
      allOf:
        - $ref: "#/components/schemas/ParentType"
        - type: object
          properties:
            foo:
              type: string
    # Bar スキーマ定義
    Bar:
      allOf:
        - $ref: "#/components/schemas/ParentType"
        - type: object
          properties:
            bar:
              type: string
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/16b17391-cbbf-4936-bade-47c47aebda3d.png)

- ポリモーフィズムを表現するためには、 Schema Object の [discriminator](https://spec.openapis.org/oas/latest.html#fixed-fields-20) を使用する
    - これは JSON Schema にはない、 OpenAPI 独自拡張のプロパティ
- `discriminator` には [Discriminator Object](https://spec.openapis.org/oas/latest.html#discriminator-object) を設定する
- Discriminator Object は、 `propertName` が必須となっている
    - これには、サブタイプを識別するための値が入ったプロパティの名前を設定する
    - ここでは `childType` と指定しているので、`childType` に入っている値によってサブタイプが決定することになる
- `propertyName` で指定したプロパティにどの値が入っていたらどのスキーマ定義になるかのマッピングは、 Discriminator Object の `mapping` で指定する
    - `mapping` は Map 型で指定する
    - キーは `propertyName` で指定したプロパティに入っている値を、バリューには対応するスキーマ定義を指定する
    - スキーマ定義はスキーマの名前か URI で指定する
        - スキーマの名前は、 `schemas` のキーで指定している値と一致するようにする
        - URI の場合は、 `$ref` で指定しているときと同じ JSON Pointer の形式で指定する
        - ここでは名前指定にしている
    - `mapping` を省略した場合は、 `propertyName` で指定したプロパティに入っている値と名前の一致するスキーマ定義が自動的に採用される
        - つまり、 `childType` の値が `Foo` なら `Foo` のスキーマ定義が採用され、値が `Bar` なら `Bar` のスキーマ定義が採用されることになる

### allOf などを使用した場合の「未定義のプロパティ」の扱い
`allOf` などで複数のスキーマ定義を組み合わせた場合、 `additionalProperties` や `additionalItems` などの「未定義のプロパティ」の扱われ方について注意が必要になる。

```yaml
schema:
  type: object
  allOf:
    - type: object
      properties:
        id: integer
  properties:
    name: string
  additionalProperties: false
```

- `allOf` を用いて `id` と `name` の2つのプロパティを組み合わせたスキーマを定義している
- さらに `additionalProperties` に `false` を設定することで、追加のプロパティを拒否している
- この定義に以下のような JSON を入力すると、 invalid と判定される

```json:NG
{
    "id": 1,
    "name": "foo"
}
```

- これは `additionalProperties` が、それが宣言されているスキーマ定義の範囲内しか適用されないことが原因となっている（`allOf` などで組み合わせたスキーマ定義までは考慮されない）
- この問題を回避するためには、 `unevaluatedProperties` を使用する

```yaml
schema:
  type: object
  allOf:
    - type: object
      properties:
        id: integer
  properties:
    name: string
  unevaluatedProperties: false
```

```json:OK
{
    "id": 1,
    "name": "foo"
}
```

```json:NG
{
    "id": 2,
    "other": "hoge"
}
```

- `unevaluatedProperties` は、「未評価のプロパティ」に対するスキーマを定義するための設定となっている
- この「未評価」とは、 `allOf` などで組み合わせたスキーマ定義を全て評価した結果、残ったまだ評価されていないプロパティを指している
- 使い方は `additionalProperties` と同じで、 `false` を指定すれば未評価のプロパティを拒否できるし、Schema Objectで定義を記述すれば未評価のプロパティの定義を制限することもできる
- 配列の場合も同様で、 `unevaluatedItems` という設定項目が用意されている


# 仕様詳細
OpenAPI の仕様（書き方）について、もうちょっと詳しく見ていく。

## 全般

### description
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    description: PathのDescription
    post:
      description: OperationのDescription
      parameters:
        - name: id
          in: query
          description: ParameterのDescription
          schema:
            type: integer
      requestBody:
        description: RequestBodyのDescription
        content:
          application/json:
            schema:
              type: object
              description: SchemaのDescription
              properties:
                id:
                  type: integer
                  description: PropertyのDescription
                name:
                  type: string
      responses:
        "200":
          description: ResponseのDescription
          content:
            application/json:
              schema:
                type: integer
```

![description.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/d97d21c3-bf1b-4542-99f8-e59495c0aa6c.jpeg)

- OpenAPI 定義は、様々な箇所に `description` というプロパティが用意されており、自然言語での説明を記載できるようになっている

#### 説明にコロンを含める
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      description: "説明: post hoge"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/68e87c05-8301-40ea-8a0b-40ab9b4eb4a7.png)

- コロン (`:`) は YAML の仕様上特別な意味を持つので、そのまま `description` の中などで使用しようとすると構文エラーになる
- 説明文中にコロンを入れたい場合は、全体をダブルクォーテーションで囲う
- もしくは、後述の複数行で書く方法でも対応できる

#### 複数行で記述する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      description: |
        a: Hello
        World

        b: Hello  
        World

        c: Hello

        World
      summary: post hoge
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/bc198758-b450-4e5a-8ec6-6bc2254fbefa.png)

- `description` の最初にパイプ (`|`) を書くと、次の行から次のプロパティ定義までは1つの文字列として認識される
- 改行は、そのままだと消される
    - Markdownと同様に半角スペース2つを末尾に書くと改行できる
    - 後述するが、これは CommonMark の仕様による
- この書き方の場合、コロンはそのまま出力される

#### CommonMark
- `description` では CommonMark というマークアップ言語を使用できる
    - CommonMark は Markdown の仕様を標準化したもの（しようとしたもの）

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      description: |
        # heading
        - hoge
          1. one
          1. two
          1. three
        - fuga
        - piyo
        - **bold**
        - *italick*
        - [link](http://localhost/test)

        `inline code`

        ```
        code
        block
        ````

        ---
        
        ## Image
        ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/9dbc3456-bf25-4149-ab9a-da28d675c04b.png)

        ## Table
        |id|name|
        |---|---|
        |1|foo|
        |2|bar|
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/40ae9331-a687-42fa-8f07-db4932fa54d1.png)

- だいたい Markdown と同じ感じで書けるっぽい
- CommonMark の詳細な仕様は [CommonMark Spec](https://spec.commonmark.org/0.27/) を参照のこと

## リクエスト

### パスごとにオペレーションで共通のパラメータを定義する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge/{foo}:
    # 共通のパラメータ定義
    parameters:
      - name: foo
        in: query
        schema:
          type: string
        example: FOO!!
    get:
      description: get hoge
    post:
      description: post hoge
      parameters:
        - name: bar
          in: query
          schema:
            type: string
    delete:
      description: delete hoge
      parameters:
        # 個々のオペレーションで上書きすることも可能
        - name: foo
          in: query
          schema:
            type: string
          example: FOO??
```

![parameters.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/624ab24f-68a6-49a8-9f4d-0df01d17883d.jpeg)

- Path Item Object の `parameters` を定義すると、そのパス配下の全てのオペレーションで共通のパラメータを定義できる
- パラメータは Operation Object の `parameters` で上書きできる
    - パラメータは `name` と `in` の組み合わせで一意に特定される
    - 上書きはできるが、削除はできない

### マルチパートの一部のデータのContent-Typeを指定する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
servers:
  - url: http://localhost:8080/echo
paths:
  /hoge:
    post:
      requestBody:
        content:
          "multipart/form-data":
            schema:
              type: object
              properties:
                foo:
                  type: string
                bar:
                  type: string
            encoding:
              # bar のコンテンツタイプを指定
              bar:
                contentType: application/json
```

```text:送信されたリクエスト（一部）
content-type: multipart/form-data; boundary=----WebKitFormBoundaryWyM7v8asGt1xfno7
...
------WebKitFormBoundaryWyM7v8asGt1xfno7
Content-Disposition: form-data; name="foo"

string
------WebKitFormBoundaryWyM7v8asGt1xfno7
Content-Disposition: form-data; name="bar"; filename="blob"
Content-Type: application/json

string
------WebKitFormBoundaryWyM7v8asGt1xfno7--
```

- [encoding](https://spec.openapis.org/oas/latest.html#media-type-encoding) を使用すると、マルチパートの特定のデータに対して `Content-Type` を付与することができる
- `encoding` は Map で指定する
    - キーにエンコーディングの情報を設定したいマルチパートのデータの名前を指定
    - 値に [Encoding Object](https://spec.openapis.org/oas/latest.html#encoding-object) を指定

## レスポンス

### デフォルトのレスポンスを定義する

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      responses:
        "200":
          description: 成功のレスポンス
        default:
          description: デフォルトのレスポンス
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/cc3946ab-9778-4947-bfe3-0f5bc0d0e337.png)

- [Operation Object](https://spec.openapis.org/oas/latest.html#operation-object) の `responses` ではステータスコードごとにレスポンスを定義できるが、特定のステータスコード以外のデフォルトのレスポンスを定義したい場合は `default` というキーを使うことで定義できる

### レスポンスのステータスコードにワイルドカードを使用する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      responses:
        "5XX":
          description: その他サーバーエラー
        "503":
          description: サーバーがダウンしてる
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/030e347e-2a29-42e4-b3e3-c6eaa952a530.png)

- レスポンスのステータスコードにはワイルドカードとして `X` が使用できる
- `5XX` なら、 500 番の任意のステータスコードを指す
    - ワイルドカードが使えるのは `1XX`, `2XX`, `3XX`, `4XX`, `5XX` の5種類のみ

### コールバックを定義する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
servers:
  - url: http://localhost:8080/echo
paths:
  /hoge:
    post:
      callbacks:
        hogeCallback:
          "http://some.host/path/to/callback":
            post:
              requestBody:
                content:
                  application/json:
                    schema:
                      type: object
                      properties:
                        id:
                          type: integer
                        value:
                          type: string
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/3e09e4ef-4fd9-45d6-84b3-d4b9fe4b0420.png)

- [Operation Object](https://spec.openapis.org/oas/latest.html#operation-object) に `callbacks` でコールバックを定義できる
- コールバックは Map で、キーにコールバック先のURL、値に [Path Item Object](https://spec.openapis.org/oas/latest.html#path-item-object) を指定する

#### コールバックとは
- まず、コールバックが何なのかについて
- コールバックとは、あるオペレーションを実行したときに API サーバーが自動的に呼び出す別の API リクエストのことを指す

```plantuml
scale 0.8

participant "APIクライアント" as client
participant "APIサーバー" as server
participant "他のAPIサーバー" as other

client -> server: APIコール
server -> other: APIコール
note right
これがコールバック
end note
```

- 最初の例の yaml だと、 `localhost` が「APIサーバー」で、 `some.host` が「他のAPIサーバー」に対応する


#### コールバックのURLをリクエストの情報から動的に構築する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      parameters:
        - name: callbackUrl
          in: query
          required: true
          schema:
            type: string
      callbacks:
        hogeCallback:
          # ランタイム式を使って動的な URL を表現
          "{$request.query.callbackUrl}":
            post:
              description: Callback
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/eecec314-cdcf-42c5-a280-cb25054bdefc.png)

- URL の部分が `{$request.query.callbackUrl}` のようになっている
- これは **ランタイム式 (runtime expression)** という OpenAPI 独自の式言語で書かれている
- 意味としては、リクエストのクエリパラメータにある `callbackUrl` というパラメータの値を参照している
- ただし、Swagger UI で出力した結果は普通に式がそのまま出力されるだけど、特別リンクになったりはしてくれない
- ランタイム式は以下のように、部分的に使用することも可能

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      requestBody:
        content:
          applicaiton/json:
            schema:
              type: object
              properties:
                id:
                  type: integer
      callbacks:
        hogeCallback:
          # ランタイム式は一部だけ利用することも可能
          "http://some.host/test?id={$request.body#/id}":
            post:
              description: Callback
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/4ac22a7f-3e23-4d58-9031-5e6858ff3245.png)

- この例では、 `"http://some.host/test?id={$request.body#/id}"` のようにクエリパラメータの `id` の値の部分をランタイム式にしている
- ここでは、リクエストボディの `id` プロパティを参照している

### ランタイム式
- コールバックで使用したランタイム式について、もう少し詳細な使い方を説明する
- 公式解説書の説明は [ここ](https://spec.openapis.org/oas/latest.html#runtime-expressions)
- ランタイム式は文字列中に波括弧 (`{}`) で囲って記載する
- ランタイム式の中では、まず以下の元となるオペレーションに含まれるいずれかの要素を参照する

|式|説明|
|---|---|
|`$url`|元となったオペレーションのURL|
|`$method`|元となったオペレーションのHTTPメソッド|
|`$statusCode`|元となったオペレーションのステータスコード|
|`$request`|元となったオペレーションのリクエスト|
|`$response`|元となったオペレーションのレスポンス|

このうち、 `$request` と `$response` についてはさらに次の4つの要素にアクセスできる。

|式|説明|例|
|---|---|---|
|`header`|ヘッダー       |`$request.header.content-type`|
|`query` |クエリパラメータ|`$request.query.id`|
|`path`  |パスパラメータ  |`$request.path.id`|
|`body`  |ボディ         |`$response.body#/id`|

- クエリパラメータやパスパラメータは、元となったオペレーションの `parameters` で定義されたパラメータの識別子と一致している必要がある
- また、 `body` の `#` から後ろは JSON Pointer でボディ内の要素を参照する

#### 具体例
以下のような定義のAPIがあったとする。

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /subscribe/{eventType}:
    post:
      parameters:
        # パスパラメータとして eventType を受け取る
        - name: eventType
          in: path
          required: true
          schema:
            type: string
        # クエリパラメータとして queryUrl をうけとる
        - name: queryUrl
          in: query
          schema:
            type: string
      requestBody:
        content:
          applicaiton/json:
            schema:
              type: object
              properties:
                failedUrl:
                  type: string
                successUrls:
                  type: array
```

- このAPIに対して、次のような HTTP リクエストを投げたとする

```http
POST /subscribe/myevent?queryUrl=https://clientdomain.com/stillrunning HTTP/1.1
Host: example.org
Content-Type: application/json
Content-Length: 188

{
  "failedUrl": "https://clientdomain.com/failed",
  "successUrls": [
    "https://clientdomain.com/fast",
    "https://clientdomain.com/medium",
    "https://clientdomain.com/slow"
  ]
}
```

- そして、レスポンスが次のような内容だったとする

```response
201 Created
Location: https://example.org/subscription/1
```

- このとき、ランタイム式と評価結果は以下のようになる

|ランタイム式|評価結果|
|---|---|
|`$url`   |`https://example.org/subscribe/myevent?queryUrl=https://clientdomain.com/stillrunning`|
|`$method`|`POST`|
|`$request.path.eventType`|`myevent`|
|`$request.query.queryUrl`|`https://clientdomain.com/stillrunning`|
|`$request.header.content-type`|`application/json`|
|`$request.body#/failedUrl`|`https://clientdomain.com/failed`|
|`$request.body#/successUrls/1`|`https://clientdomain.com/medium`|
|`$response.header.Location`|`https://example.org/subscription/1`|

## タグ
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    get:
      tags: ["aaa"]
    post:
      tags: ["bbb"]
    put:
      tags: ["aaa", "ccc"]
    delete:
      tags: []
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/265eedcd-4995-402c-a73c-b9c9d12b3691.png)

- [Operation Object](https://spec.openapis.org/oas/latest.html#operation-object) には `tags` でタグを割り当てることができる
- タグを使用すると、オペレーションをグルーピングして管理できる
    - 生成されるドキュメント上では、タグごとにオペレーションがまとめられて出力される(Swagger UI の場合)
    - タグの順序はツール依存で不定（Swagger UI の場合は辞書順になっているっぽい）
        - 後述するが、順序を指定することも可能
    - 同じタグが複数のオペレーションに割り当てられている場合は、それぞれのタグの下に同じオペレーションが重複して出力される
        - 上述の例だと `PUT` が重複して出力されている
    - タグが指定されていないオペレーションは `default` というグループに割り当てられる

### タグの順序を指定する
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    get:
      tags: ["aaa"]
    post:
      tags: ["bbb"]
    put:
      tags: ["aaa", "ccc"]
    delete:
      tags: []
tags:
  - name: bbb
    description: bbbのdescription
  - name: ccc
    description: cccのdescription
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/5cfee0e4-fa28-4f72-aa2e-8c7d620e6c39.png)

- [OpenAPI Object](https://spec.openapis.org/oas/latest.html#openapi-object) の `tags` で、タグの順序を定義できる
- `tags` は [Tag Object](https://spec.openapis.org/oas/latest.html#tag-object) を配列で指定する
- Tag Object は `name` が必須で、オペレーションの `tags` で指定したものと同じ名前になるように値を設定する
    - `description` で説明をつけることも可能
- この配列で指定した順序が、そのままドキュメント上でのタグの順序として反映される
- 実際にオペレーションで使用しているタグを全て定義する必要はない
    - 未定義のタグは、順不同（ツール依存）となる

## webhooks
```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
webhooks:
  # キーは Webhook を一意に識別する任意の文字列を指定
  hoge:
    post:
      description: post hoge
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: integer
                value:
                  type: string
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/58b0212e-086d-456d-b2e2-020a55da927b.png)

- [OpenAPI Object](https://spec.openapis.org/oas/latest.html#openapi-object) の `webhooks` で Webhook の仕様を定義できる
- `webhooks` は Map で指定し、キーに Webhook を一意に識別する名前、値に [Path Item Object](https://spec.openapis.org/oas/latest.html#path-item-object) を指定する

## 認証方法の定義
### 基本(APIキー認証)

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
servers:
  - url: http://localhost:8080/echo
paths:
  /hoge:
    post:
      description: post hoge
components:
  securitySchemes:
    apiKeyExample:
      type: apiKey
      name: X-Api-Key
      in: header
security:
  - apiKeyExample: []
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/8b42df7b-5217-4093-956e-ce82682a58d9.png)

- 右上の [Authorize] というボタンをクリックすると、次のようなダイアログが表示される

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/cbb35c8b-4f3d-4329-ab44-d6731de364c7.png)

- [Value] のところに適当な値を入れて [Authorize] ボタンをクリックする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/bb20bd53-df40-4980-b82b-37fd193d58a4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/8cbcb1cb-9edc-43c6-969c-d17dc2864c7d.png)

- 値が入力された状態になるので、 [Close] でダイアログを閉じる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/608b65df-ff53-4ed0-8599-6f16037431c6.png)

- [Authorize] ボタンの鍵マークがロックされた状態になっているので、この状態で [Try it] で API を実行してみる

```text:サーバーに届いたリクエストの情報
## Method
POST
## Request URI
requestURI = /echo/hoge
queryString = 

## Headers
host: localhost:8080
connection: keep-alive
...
x-api-key: TestApiKey
...
```

- `x-api-key` というヘッダーに先ほどダイアログで入力した値が設定された状態でリクエストが届いている

#### 説明
```yaml
components:
  securitySchemes:
    apiKeyExample:
      type: apiKey
      name: X-Api-Key
      in: header
security:
  - apiKeyExample: []
```

- API の認証方法を定義するには、次の2つの要素を定義する必要がある
    - `components.securitySchemas`
    - `security`
- まず `components.securitySchemas` で認証方法の具体的な仕様を定義する
- 次に `security` で、 `securitySchemas` で定義した認証方法のうち、実際に使用する認証方法を宣言する

##### securitySchemes
```yaml
components:
  securitySchemes:
    apiKeyExample:
      type: apiKey
      name: X-Api-Key
      in: header
```

- `secruitySchemas` は Map で、キーに認証方法を一意に識別できる名前を、値に [Security Scheme Object](https://spec.openapis.org/oas/latest.html#security-scheme-object-0) を指定する
- Security Schema Object では、まず認証の種類を `type` で指定する
- `type` には以下のいずれかを指定できる

|値|認証方法|
|---|---|
|`apiKey`   |APIキーを用いた認証|
|`http`     |HTTP認証|
|`mutualTLS`|相互TLS認証|
|`oauth2`   |OAuth 2.0|
|`openIdConnect` |OpenID Connect|

- `apiKey` は、例でも示したように独自ヘッダーなどを用いて認証用のキー情報を連携する認証方法になる
- `apiKey` を指定した場合、 `in` と `name` を追加で指定する必要がある
    - `in` には、APIキーをどのパラメータから渡すかを設定する
        - `query`, `header`, `cookie` のいずれかが指定できる
    - `name` には、 API キーを設定するパラメータの名前を設定する
        - ここでは `in` を `header` にしているので、ヘッダーの名前を指定していることになる

##### security
```yaml
security:
  - apiKeyExample: []
```

- `security` には、実際にAPIリクエスト時に使用する認証方法を指定する
- `security` は配列で指定し、配列の各要素は [Security Requirement Object](https://spec.openapis.org/oas/latest.html#security-requirement-object) で指定する
- Security Requirement Object は、キーに `securitySchemas` で定義した認証方法の識別名を、値には認証で使用する追加のパラメータを `string` の配列で渡す
    - ただし、追加のパラメータの要否は認証の種類によって異なっている
    - `apiKey` の場合、追加のパラメータは必要ないので空の配列を渡しておけばいい
    - 認証の種類が `oauth2` か `openIdConnect` の場合、ここにスコープの配列を渡すことになる

### HTTP認証(Basic認証)

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
servers:
  - url: http://localhost:8080/echo
paths:
  /hoge:
    post:
      description: post hoge
components:
  securitySchemes:
    basicExample:
      type: http
      scheme: basic
security:
  - basicExample: []
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/aa4523e3-ab4c-44ef-adea-ce32a2f249e0.png)

```text:サーバーに届いたHTTPリクエスト
## Method
POST
## Request URI
requestURI = /echo/hoge
queryString = 

## Headers
...
authorization: Basic dGFybzpwYXNzd29yZA==
...
```

- 上記はHTTP認証の、特にBasic認証の場合の例になる
- HTTP認証の場合は `type` に `http` を指定する
- HTTP認証の場合、 `schema` が必須指定となる
    - `schema` には使用するHTTP認証スキームの名前を指定する
    - この名前は [IANA Authentication Scheme registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml) に登録されている名前のいずれかを使用する
    - ここではBasic認証にするので `basic` と指定している
- Swagger UI から入力できる認証情報がユーザー名とパスワードになっており、Try itでリクエストを送ると、 `authorization` ヘッダーにBasic認証の情報が載っていることがわかる

### OpenID Connect

検証は以下のような構成で試した。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/75a8545c-97f2-4269-9269-8111cd8297e2.png)


- WSL2 上の Docker で Keycloak と Swagger-UI を動かし、認可コードグラントフローでOIDCの認証を行いAPIアクセスを行う
- Keycloak と Swagger-UI の画面操作でブラウザを分けているのは、管理画面操作用のユーザーの認証情報がブラウザにあるとOIDC用のユーザーの認証とごっちゃになって面倒なので
- カッコ内の `8080` とかはポート番号

#### Keycloakの導入
認可サーバーである Keycloak を導入し、連携用のクライアントの登録と認証用のユーザーの作成を行う。


```yaml:compose.yml
services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.2.4
    command: start-dev
    ports:
      - "8080:8080"
    environment:
      KC_BOOTSTRAP_ADMIN_USERNAME: admin
      KC_BOOTSTRAP_ADMIN_PASSWORD: admin
```

- これを `docker compose up` で起動
- `localhost:8080` にアクセス
- サインイン画面が表示されるので `admin`/`admin` でサインイン
- 左のメニューから [Manage realms] を選択し、 [Create realm] ボタンをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/605dc44d-050b-4494-b9b2-63a923bf282f.png)

- [Realm name] に `test-realm` と入力して [Create] ボタンをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/e5389f24-6488-4223-a360-173ba5a9a03e.png)

- メニューの左上の [Current Realm] が `test-realm` になっていることを確認して [Manage] > [Clients] をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/4d9b0cb1-dd07-4c70-a12f-e6285c1d2c98.png)

- [Creat client] ボタンをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/b0aae25a-f072-47f2-a628-7fc415ef14a3.png)

- 以下の要領で入力してクライアントを作成
    - 記載のない項目はデフォルトのまま

|項目|設定値|
|---|---|
|Client type|OpenID Connect|
|Client ID|`swagger-client`|
|Client authentication|オン|
|Authentication flow|Standard flow|
|Valid redirect URIs|`http://localhost:8081/oauth2-redirect.html`|
|Web origins|`*`|

[Valid redirect URIs] に設定している URL は、 Swaggre UI に戻るときのリダイレクト先となっている

- メニューの [Clients] を選択すると、今作成した `swagger-client` が追加されているので、選択する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/cb8dba97-66d3-4a71-9645-d8284cb2ac89.png)

- [Credentials] タブを開き、 [Client Secret] の値を控える(後で Swagger-UI の方で設定する)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/04ac38ba-930f-41b6-99ef-8556d929f37a.png)


---

続いてユーザーを作成していく。

- メニューの [Users] を選択
- [Create new user] をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/7059437e-bc35-4e29-bf69-b444a7750bc0.png)

- [Username] に `test-user` と入力して [Create] をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/85824cd1-d9f5-4b09-8c04-218da7487f2b.png)

- 作成された `test-user` の編集画面が開くので、 [Credentials] タブを開き [Set password] ボタンをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/7763e55f-a214-499e-b1c2-6c389ec306fe.png)

- パスワードはとりあえず `password` にしておく
- [Temporary] のチェックはオフにして [Save] をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/cb1e406c-0a37-4ca1-9c0d-e31246e9c4b4.png)

#### Swagger-UI の導入
```yaml:test.yml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
servers:
  - url: http://localhost:8082/echo
paths:
  /hoge:
    post:
      description: post hoge
components:
  securitySchemes:
    oidcExample:
      type: openIdConnect
      openIdConnectUrl: http://localhost:8080/realms/test-realm/.well-known/openid-configuration
security:
  - oidcExample: []
```

- このファイルをWSL2から参照可能な任意の場所に置く
- 続いて、このyamlファイルと同じ場所に `compose.yml` を作成する
- 内容は以下

```yaml:compose.yml
services:
  swagger-ui:
    image: docker.swagger.io/swaggerapi/swagger-ui:v5.21.0
    ports:
      - "8081:8080"
    environment:
      SWAGGER_JSON: /test.yml
    volumes:
      - ./test.yml:/test.yml
```

- `docker compose up` で起動しておく

#### Spring Boot Application の導入
- 目的は Swagger-UI 側の認証の動作を見ることなので、 Spring Boot Application についてはややこしいことはあんまりしない
    - JWTのトークンチェックとかはしない
- ただし CORS は通るようにしないといけないので、そこはとりあえず全通しの設定を入れておく

```java:CORS全通しの構え
package sandbox.echo.server;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class Config implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedHeaders("*")
                .allowedOrigins("*")
                .allowedMethods("*");
    }
}
```

- あとは、 `/echo` 以下任意のリクエストを受け付けてリクエストの情報をコンソールに書き出すコントローラを用意

```java:雑なechoコントローラ
package sandbox.echo.server;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Enumeration;

@RestController
public class EchoController {

    @RequestMapping("/echo/*")
    public String echo(HttpServletRequest request) {
        System.out.println();
        System.out.println("###############################################");
        // method
        System.out.println("## Method");
        System.out.println(request.getMethod());
        // url
        System.out.println("## Request URI");
        System.out.println("requestURI = " + request.getRequestURI());
        System.out.println("queryString = " + ((request.getQueryString() != null) ? request.getQueryString() : ""));
        // headers
        System.out.println();
        System.out.println("## Headers");
        final Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            final String headerName = headerNames.nextElement();
            final Enumeration<String> headers = request.getHeaders(headerName);
            while (headers.hasMoreElements()) {
                final String headerValue = headers.nextElement();
                System.out.println(headerName + ": " + headerValue);
            }
        }

        return "Hello";
    }
}
```

- これを `8082` ポートで起動しておく

#### 動作確認

- Keycloak の管理画面を開いているのとは別のブラウザで `http://localhost:8081` にアクセスする
    - 前述のとおり、管理画面のセッションが残っているとややこしいので、そこがクリアできるなら何でもいい

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/5e966254-abc1-4798-80d3-10e474679ad9.png)

- [Authorize] ボタンをクリックする
- ダイアログのスクロールが真ん中らへんに来ている場合は、一番上の [oidcExample (OAuth2, authorization_code)] まで移動する
- [client_id] に `swagger-client`、 [client_secret] に先ほど作成したクライアントシークレットを入力
- [Authorize] ボタンをクリック
- Keycloack のサインイン画面が開くので、 `test-user`/`password` と入力して [Sign In] ボタンをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/d5b1083c-b034-4320-b24d-29d785e85ed5.png)

- 初回はユーザー情報の入力が求められるので、適当に入力して [Submit]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/5e487b03-23fb-4d80-8a88-9d26c99b2bf7.png)

- 認証が完了したら [Close] でダイアログを閉じる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/51a9e7f5-b8c4-4a34-a9b8-442218e0d78b.png)

- Swagger-UI の画面から [Try it] でAPIを叩いてみる

```text:サーバーに届いたリクエストの情報
###############################################
## Method
POST
## Request URI
requestURI = /echo/hoge
queryString = 

## Headers
host: localhost:8082
connection: keep-alive
content-length: 0
sec-ch-ua-platform: "Windows"
authorization: Bearer eyJhbGciOiJSUzI1NiI...(中略)...g4xiJZIslUJgQ
(省略)
```

- `authorization` ヘッダーで JWT のトークンが渡されていることがわかる


##### 説明
```yaml
components:
  securitySchemes:
    oidcExample:
      type: openIdConnect
      openIdConnectUrl: http://localhost:8080/realms/test-realm/.well-known/openid-configuration
security:
  - oidcExample: []
```

- OpenID Connect の場合、 `type` は `openIdConnect` とする
- OpenID Connect の場合、 `openIdConnectUrl` が必須となるのでディスカバリーエンドポイントの URL を指定する
- `security` の配列には実行に必要な scope を指定する
    - 今回は、認可は特に試してないので空にしている

# Swagger UI
[Swagger UI](https://github.com/swagger-api/swagger-ui) は OpenAPI のAPI定義ファイルから HTML のドキュメントを生成するためのツール。

Swagger UI は JavaScript のモジュールとして提供され、 Web アプリの中に組み込む形で利用する。
コマンドラインツールに定義ファイルを食わせたら HTML が出力される、みたいな形式**ではない**。

## ドキュメント生成
Swagger UI を使って作成したドキュメントアプリを Tomcat にデプロイして見られるようにする。
Tomcat は 11.0.7 を使用した。

### Swagger UI をダウンロードする
- まず、[Swagger UI の GitHub リポジトリ](https://github.com/swagger-api/swagger-ui) をローカルに落とす
- 落とし方は git で clone でもいいし、 [Releases](https://github.com/swagger-api/swagger-ui/releases) からソースコードの zip をダウンロードするでもいい

### dist フォルダを抜き出す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/9ae4f74c-be02-478a-98a8-38a692988cf2.png)

- ダウンロードした中に `dist` というフォルダがあるので、これを抜き出す

### Tomcat に配備する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/cd00482a-955a-46de-b9a8-06d6f1eaf12a.png)

- 今回は Tomcat で動かすので `webapps` の下に `dist` をコピーして、 `swagger` という名前にフォルダをリネームする
- `swagger` フォルダの中は以下のような形で `dist` の中身そのまま

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/8c3e4adb-da98-402c-994c-e62c2ff7993a.png)

### ドキュメント化したいAPI定義ファイルを配置する
```yaml:sample.yml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    get:
      description: get hoge
    post:
      description: post hoge
  /fuga:
    delete:
      description: delete fuga
```

- 動作確認のための簡単な定義ファイル
- これを、先ほど配置した Web アプリのフォルダ (`swagger`) の中に配置する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/5d396ee2-d5ea-45c8-9d06-345e6c66caf7.png)

### swagger-initializer.js を修正する
- `swagger-initializer.js` というファイルがあるので、これをテキストエディタで開く
- `SwaggerUIBundle` 関数の引数に渡している `url` を、さきほど配置した `sample.yml` に変更する

```js:swagger-initializer.js
window.onload = function() {
  //<editor-fold desc="Changeable Configuration Block">

  // the following lines will be replaced by docker/configurator, when it runs in a docker-container
  window.ui = SwaggerUIBundle({
    url: "sample.yml", // ★ここを、読み込むAPI定義ファイルに変更する
    dom_id: '#swagger-ui',
    deepLinking: true,
    presets: [
      SwaggerUIBundle.presets.apis,
      SwaggerUIStandalonePreset
    ],
    plugins: [
      SwaggerUIBundle.plugins.DownloadUrl
    ],
    layout: "StandaloneLayout"
  });

  //</editor-fold>
};
```

### 動作確認
- Tomcat を起動し、 `http://localhost:8080/swagger` にブラウザでアクセスする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/6cbc7a13-f723-44a6-9d05-3fcb892d401a.png)

- `sample.yml` の内容で HTML ドキュメントにアクセスできる

## Docker で生成する
Docker を使うことで簡単に確認することもできる

```bash
$ docker run \
  --name swagger-ui \
  --rm \
  -p 8080:8080 \
  -v path/to/test.yml:/test.yml \
  -e SWAGGER_JSON=/test.yml \
  docker.swagger.io/swaggerapi/swagger-ui
```

- 読み込ませたい YAML ファイルを `-v` でコンテナ内の `/test.yml` に配置し、環境変数 `SWAGGER_JSON` でそのパスを指定している
    - JSON じゃないけど `SWAGGER_JSON` でいい
- 起動したら `http://localhost:8080` にアクセスする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/a2e78f3a-4938-4332-9547-8f898811908e.png)

# コード生成(OpenAPI Generator)
## Swagger Codegen と OpenAPI Generator
- OpenAPI の API 定義ファイルからクライアントやサーバーのソースコードを自動生成するツールがいくつか存在する
- 有名なものとして [Swagger Codegen](https://github.com/swagger-api/swagger-codegen) や [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) がある
- もともとは Swagger Codegen として開発されていたが、OpenAPI 3.0 になかなか対応しないとか管理している会社による強引な修正がされるようになって有志がフォークしてできたのが OpenAPI Generator らしい
    - このあたりの詳しい話は [平静を保ち、コードを生成せよ 〜 OpenAPI Generator誕生の背景と軌跡 〜 / gunmaweb34 - Speaker Deck](https://speakerdeck.com/akihito_nakano/gunmaweb34) にまとめられているので一読の価値あり
- 一応、現在は Swagger Codegen も OpenAPI 3.0 以上に対応しているようだが、開発がよりアクティブなのは OpenAPI Generator のほうなので、ここでは OpenAPI Generator の使い方について調べることにする

## インストール
- OpenAPI Generator の実体は Java アプリで、 jar ファイルを落としてきてコマンドラインから利用する
- jar ファイルは [Maven のセントラルリポジトリ](https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/) から `openapi-generator-cli-x.x.x.jar` をダウンロードして入手する
- 実行には Java 11 以上が必要

```bash:動作確認
$ java -jar openapi-generator-cli-7.13.0.jar help
usage: openapi-generator-cli <command> [<args>]

The most commonly used openapi-generator-cli commands are:
    author        Utilities for authoring generators or customizing templates.
    batch         Generate code in batch via external configs.
    config-help   Config help for chosen lang
    generate      Generate code with the specified generator.
    help          Display help information about openapi-generator
    list          Lists the available generators
    meta          MetaGenerator. Generator for creating a new template set and configuration for Codegen.  The output will be based on the language you specify, and includes default templates to include.
    validate      Validate specification
    version       Show version information used in tooling

See 'openapi-generator-cli help <command>' for more information on a specific
command.
```

## Docker で動かす
- [Docker イメージ](https://hub.docker.com/r/openapitools/openapi-generator-cli/) が提供されているので、 Docker で動かすことも可能

```bash
$ docker run --rm openapitools/openapi-generator-cli:v7.13.0 help
usage: openapi-generator-cli <command> [<args>]

The most commonly used openapi-generator-cli commands are:
    author        Utilities for authoring generators or customizing templates.
    batch         Generate code in batch via external configs.
    config-help   Config help for chosen lang
    generate      Generate code with the specified generator.
    help          Display help information about openapi-generator
    list          Lists the available generators
    meta          MetaGenerator. Generator for creating a new template set and configuration for Codegen.  The output will be based on the language you specify, and includes default templates to include.
    validate      Validate specification
    version       Show version information used in tooling

See 'openapi-generator-cli help <command>' for more information on a specific
command.
```

## 基本的な使い方
- OpenAPI Generator にはいくつかのコマンドが用意されていて、コマンド名とパラメータを渡して使用する
- コマンドの例
    - `generate`
        - サーバーやクライアントのソースコードを生成する
    - `list`
        - generator のリストを表示する
    - `help`
        - ヘルプ
        - コマンドを引数に渡すことで、そのコマンドのヘルプの詳細を確認できる

### generate コマンド
```bash:コマンド例
$ java -jar openapi-generator-cli-7.13.0.jar \
    generate \
    -g jaxrs-spec \
    -i sample.yml \
    -o output/jaxrs-spec
```

- ソース生成に使用するコマンド
- `-g` オプションで **generator** を指定する
    - generator は出力する生成物の種類ごとに用意されている
    - 例えば JAX-RS で実装されたサーバーサイドのソースを生成したい場合は `jaxrs-spec` を指定する
    - generator の一覧は [こちらのページ](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/README.md) で確認できる
        - もしくは `list` コマンドでも確認できる
- `-i` で、OpenAPI の API 定義ファイルを指定する
- `-o` は出力先のディレクトリを指している
- 以下のようなファイルたちが出力される

```text:出力結果
│  .openapi-generator-ignore
│  pom.xml
│  README.md
│
├─.openapi-generator
│      FILES
│      VERSION
│
└─src
    ├─gen
    │  └─java
    │      └─org
    │          └─openapitools
    │              ├─api
    │              │      HogeApi.java
    │              │      RestApplication.java
    │              │      RestResourceRoot.java
    │              │
    │              └─model
    │                      HogeGet200Response.java
    │
    └─main
        └─openapi
                openapi.yaml
```

```java:HogeApi.java(見やすさのため一部改行を調整)
package org.openapitools.api;

import org.openapitools.model.HogeGet200Response;

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

import io.swagger.annotations.*;

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

/**
* Represents a collection of functions to interact with the API endpoints.
*/
@Path("/hoge")
@Api(description = "the hoge API")
@javax.annotation.Generated(
    value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen",
    date = "2025-05-17T20:50:38.765988100+09:00[Asia/Tokyo]",
    comments = "Generator version: 7.13.0"
)
public class HogeApi {

    @GET
    @Produces({ "application/json" })
    @ApiOperation(value = "", notes = "", response = HogeGet200Response.class, tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "200 OK", response = HogeGet200Response.class)
    })
    public Response hogeGet(@QueryParam("id")   Integer id) {
        return Response.ok().entity("magic!").build();
    }
}

```

- Maven のプロジェクトになっていて、OpenAPI の API 定義を満たす形で JAX-RS のソースが出力されている

### help コマンド
```bash
$ java -jar openapi-generator-cli-7.13.0.jar help generate
NAME
        openapi-generator-cli generate - Generate code with the specified
        generator.

SYNOPSIS
        openapi-generator-cli generate
                [(-a <authorization> | --auth <authorization>)]
                [--api-name-suffix <api name suffix>] [--api-package <api package>]
                [--artifact-id <artifact id>] [--artifact-version <artifact version>]
                [(-c <configuration file> | --config <configuration file>)] [--dry-run]
                [(-e <templating engine> | --engine <templating engine>)]
                [--enable-post-process-file]
                [--enum-name-mappings <enum name mappings>...]
                [(-g <generator name> | --generator-name <generator name>)]
                [--generate-alias-as-model] [--git-host <git host>]
（省略）
```

- `help <コマンド>` と指定することで、コマンドごとの細かい説明を確認できる

### config-help コマンド
```bash
$ java -jar openapi-generator-cli-7.13.0.jar config-help -g jaxrs-spec
CONFIG OPTIONS

        additionalEnumTypeAnnotations
            Additional annotations for enum type(class level annotations)

        additionalModelTypeAnnotations
            Additional annotations for model type(class level annotations). List separated by semicolon(;) or new line (Linux or Windows)

        additionalOneOfTypeAnnotations
            Additional annotations for oneOf interfaces(class level annotations). List separated by semicolon(;) or new line (Linux or Windows)

        allowUnicodeIdentifiers
            boolean, toggles whether unicode identifiers are allowed in names or not, default is false (Default: false)
(省略)
```

- `config-help` コマンドを使用すると、個々の generator の細かい設定についてのヘルプを確認できる
- `-g` オプションで、確認したい generator を指定する
- generator の細かい設定の説明は、各 generator の README でも確認できる
    - 例: [jaxrs-spec の README](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/jaxrs-spec.md)




## サーバーサイド

### どの generator を使う？
- Java でサーバーサイドの RESTful Web API の実装といえば JAX-RS か Spring Web MVC が個人的には多い気がするので、これらの実装を生成する方法について調べる
- Spring だと、該当する generator は [spring](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/spring.md) だけみたいなのでいいが、 JAX-RS は何かいっぱいある
    - jaxrs-cxf
    - jaxrs-cxf-cdi
    - jaxrs-cxf-extended
    - jaxrs-jersey
    - jaxrs-resteasy
    - jaxrs-resteasy-eap
    - jaxrs-spec
- cxf は [Apache CXF](https://cxf.apache.org/) という Apache プロジェクトにおける JAX-RS 実装らしい
- cxf, jersey, resteasy と付いている generator は、それぞれの実装に依存した形でソースが生成されるのだろう（たぶん）
- 唯一、具体的な実装名が含まれていない jaxrs-spec は、特定の実装ライブラリに依存しない JAX-RS で定義されたAPIのみを利用した形でソースが生成される
- ということで、基本は jaxrs-spec を選択すればいい気がする
    - resteasy に関する generator の出力については [OpenAPI GeneratorでJAX-RS（RESTEasy）のサーバーサイドのソースコードを生成してみる - CLOVER🍀](https://kazuhira-r.hatenablog.com/entry/2024/10/14/190915) で比較がされているの参考までに

### jaxrs-spec ジェネレータ
#### 生成されるファイル
以下のAPI定義を元にソースを生成してみる。

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /foo/{id}:
    post:
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 9999
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                value:
                  type: string
                  pattern: ^[a-zA-Z0-9]+$
      responses:
        "200":
          description: 200 OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
```

生成されたのは以下。

```text:生成されたファイル
└─src
    ├─gen
    │  └─java
    │      └─org
    │          └─openapitools
    │              ├─api
    │              │      FooApi.java
    │              │      RestApplication.java
    │              │      RestResourceRoot.java
    │              │
    │              └─model
    │                      FooIdPost200Response.java
    │                      FooIdPostRequest.java
    │
    └─main
        └─openapi
                openapi.yaml
```

- `RestApplication` は JAX-RS の Application クラス
- `FooApi` が `paths` の定義を元に生成されたリソースクラスのファイルで、以下のようになっている（見やすくするため、一部改行を調整している）

```java:FooApi.java
package org.openapitools.api;

import org.openapitools.model.FooIdPost200Response;
import org.openapitools.model.FooIdPostRequest;

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

import io.swagger.annotations.*;

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

/**
* Represents a collection of functions to interact with the API endpoints.
*/
@Path("/foo/{id}")
@Api(description = "the foo API")
@javax.annotation.Generated(
    value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen",
    date = "2025-05-27T20:47:07.506280700+09:00[Asia/Tokyo]",
    comments = "Generator version: 7.13.0"
)
public class FooApi {

    @POST
    @Consumes({ "application/json" })
    @Produces({ "application/json" })
    @ApiOperation(value = "", notes = "", response = FooIdPost200Response.class, tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "200 OK",
        response = FooIdPost200Response.class)
    })
    public Response fooIdPost(
        @PathParam("id") @Min(1) @Max(9999) Integer id,
        @Valid FooIdPostRequest fooIdPostRequest
    ) {
        return Response.ok().entity("magic!").build();
    }
}
```

- API定義に従って JAX-RS のアノテーションが設定されているのが分かる
- また、 `@Min` など Bean Validation のアノテーションもついている
    - とはいえ、 JSON Schema で表現できる全ての制約が自動生成でカバーできるわけもなく、 `dependentRequried` など Bean Validation の単純なアノテーション設定だけでは表現できない制限については無視される
- リクエストオブジェクトの `FooIdPostRequest` は以下のような感じ

```java:FooIdPostRequest.java
package org.openapitools.model;

import com.fasterxml.jackson.annotation.JsonTypeName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import javax.validation.constraints.*;
import javax.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;



@JsonTypeName("_foo__id__post_request")
@javax.annotation.Generated(
    value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen",
    date = "2025-05-27T20:47:07.506280700+09:00[Asia/Tokyo]",
    comments = "Generator version: 7.13.0"
)
public class FooIdPostRequest   {
  private String value;

  public FooIdPostRequest() {
  }

  /**
   **/
  public FooIdPostRequest value(String value) {
    this.value = value;
    return this;
  }

  
  @ApiModelProperty(value = "")
  @JsonProperty("value")
   @Pattern(regexp="^[a-zA-Z0-9]+$")public String getValue() {
    return value;
  }

  @JsonProperty("value")
  public void setValue(String value) {
    this.value = value;
  }


  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    FooIdPostRequest fooIdPostRequest = (FooIdPostRequest) o;
    return Objects.equals(this.value, fooIdPostRequest.value);
  }

  @Override
  public int hashCode() {
    return Objects.hash(value);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("class FooIdPostRequest {\n");
    
    sb.append("    value: ").append(toIndentedString(value)).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    ");
  }
}
```

#### インタフェースのみ出力する
- デフォルトでは、リソースクラスは実体のあるクラスとして生成される
- しかし、これだと再生成したときに手修正した箇所を手動でマージしなくてはならなくて都合が悪い
- そこで、インタフェースのみを出力するオプションが用意されている
- これを利用すると、ジェネレーションギャップ・パターンを利用できるようになる
- まず、以下のような内容の設定ファイル(YAML)を用意する

```yaml:config.yml
interfaceOnly: true
```

- この YAML ファイルを指定して、 generate コマンドを実行する
- 設定ファイルは `-c` オプションで指定する

```bash
java -jar openapi-generator-cli-7.13.0.jar generate \
  -i sample.yml \
  -g jaxrs-spec \
  -c config.yml \
  -o output
```

- すると、リソースクラスは以下のようにインタフェースで出力される

```java:FooApi.java
package org.openapitools.api;

import org.openapitools.model.FooIdPost200Response;
import org.openapitools.model.FooIdPostRequest;

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

import io.swagger.annotations.*;

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

/**
* Represents a collection of functions to interact with the API endpoints.
*/
@Path("/foo/{id}")
@Api(description = "the foo API")
@javax.annotation.Generated(
    value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen",
    date = "2025-05-27T21:29:46.858104500+09:00[Asia/Tokyo]",
    comments = "Generator version: 7.13.0"
)
public interface FooApi {

    /**
     * 
     *
     * @param id 
     * @param fooIdPostRequest 
     * @return 200 OK
     */
    @POST
    @Consumes({ "application/json" })
    @Produces({ "application/json" })
    @ApiOperation(value = "", notes = "", tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "200 OK",
        response = FooIdPost200Response.class)
    })
    FooIdPost200Response fooIdPost(
        @PathParam("id") @Min(1) @Max(9999) Integer id,
        @Valid FooIdPostRequest fooIdPostRequest
    );

}
```

#### Jakarta EE で出力する
- デフォルトは Java EE のパッケージ(`javax.*`)で出力される
- Jakarta EE の場合はパッケージが変わっている(`jakarta.*`)
- Jakarta EE のパッケージで出力する場合は、以下のように `useJakartaEe` オプションに `true` を設定する

```yaml:config.yml
useJakartaEe: true
```

- 生成されたリソースクラスは以下のようになる

```java:FooApi.java
package org.openapitools.api;

import org.openapitools.model.FooIdPost200Response;
import org.openapitools.model.FooIdPostRequest;

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("/foo/{id}")
@Api(description = "the foo API")
@jakarta.annotation.Generated(
    value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen",
    date = "2025-05-29T11:16:48.278034900+09:00[Asia/Tokyo]",
    comments = "Generator version: 7.13.0"
)
public class FooApi {

    @POST
    @Consumes({ "application/json" })
    @Produces({ "application/json" })
    @ApiOperation(value = "", notes = "", response = FooIdPost200Response.class, tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "200 OK", response = FooIdPost200Response.class)
    })
    public Response fooIdPost(
        @PathParam("id") @Min(1) @Max(9999) Integer id,
        @Valid FooIdPostRequest fooIdPostRequest
    ) {
        return Response.ok().entity("magic!").build();
    }
}
```

- `jakarta.*` パッケージになっている

#### その他オプション

- jaxrs-spec のオプション全量は [こちら](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/jaxrs-spec.md)
- この中から、個人的に使うことになりそうだなぁと思ったものをピックアップする

|オプション|デフォルト値|説明|
|---|---|---|
|`apiPackage`|`org.openapitools.api`|リソースクラスの出力先パッケージ|
|`dateLibrary`|`legacy`|日付クラスに何を使うか。<br>**joda**: Joda<br>**legacy**: `java.util.Date`<br>**java8-localtime**: `LocalDateTime`<br>**java8**: JSR310のクラス|
|`generateBuilders`|`false`|モデルクラスのビルダーを生成するかどうか|
|`modelPackage`|`org.openapitools.model`|モデルクラスの出力先パッケージ|
|`useBeanValidation`|`true`|Bean Validationを使用するかどうか|

#### ポリモーフィズムには非対応？

- 以下のようなAPI定義を元にソースを生成してみる

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      requestBody:
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/Foo"
                - $ref: "#/components/schemas/Bar"
      responses:
        "200":
          description: 200 OK
components:
  schemas:
    # 親スキーマ定義
    ParentType:
      type: object
      properties:
        childType:
          type: string
      required: ["childType"]
      discriminator:
        # サブタイプを決定するための値が格納されたプロパティ名を指定する
        propertyName: childType
        # propertyName で指定したプロパティの値とサブタイプのスキーマとのマッピングを定義する
        mapping:
          # childType の値が foo なら Foo スキーマ
          foo: Foo
          # childType の値が bar なら Bar スキーマ
          bar: Bar
    # Foo スキーマ定義
    Foo:
      allOf:
        - $ref: "#/components/schemas/ParentType"
        - type: object
          properties:
            foo:
              type: string
    # Bar スキーマ定義
    Bar:
      allOf:
        - $ref: "#/components/schemas/ParentType"
        - type: object
          properties:
            bar:
              type: string
```

- なお、継承関係のイメージは以下のような感じ

```plantuml
class ParentType {
  childType: String
}

class Foo extends ParentType {
  foo: String
}

class Bar extends ParentType {
  bar: String
}

object "foo: Foo" as foo {
  childType = "foo"
  foo = "xxxx"
}

object "bar: Bar" as bar {
  childType = "bar"
  bar = "yyyy"
}
```

- これを生成すると、リソースクラスの引数が `HogePostRequest` という型で生成されるが、その内容が以下のようになっている

```java:HogePostRequest.java
public class HogePostRequest   {
  private String childType;
  private String bar;
```

- `bar` フィールドしか持っていなくて、 `foo` を受け取れない形になっている
- このままだと、 `Foo` 型のオブジェクトを受け取れないことになる
- なんかポリモーフィズムが動かないという Issue は Open のまま存在しているが、ここで記載している内容とはちょっと違うっぽい
    - [[BUG] jaxrs-spec [server] oneOf, anyOf Polymorphism does not work #5565](https://github.com/OpenAPITools/openapi-generator/issues/5565)
- ちなみに、 spring ジェネレータの場合はいい感じに出力してくれるので jaxrs-spec 固有のバグなんじゃないかなぁと思っている

### spring ジェネレータ

- jaxrs-spec ジェネレータの検証で使用していたものと同じ yaml を使って spring ジェネレータでソースを生成してみる

```yaml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /foo/{id}:
    post:
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 9999
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                value:
                  type: string
                  pattern: ^[a-zA-Z0-9]+$
      responses:
        "200":
          description: 200 OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
```

- 生成されたファイルは以下のようになっている

```text:生成されたファイル
│  .openapi-generator-ignore
│  pom.xml
│  README.md
│
├─.openapi-generator
│      FILES
│      VERSION
│
└─src
    ├─main
    │  ├─java
    │  │  └─org
    │  │      └─openapitools
    │  │          │  OpenApiGeneratorApplication.java
    │  │          │  RFC3339DateFormat.java
    │  │          │
    │  │          ├─api
    │  │          │      ApiUtil.java
    │  │          │      FooApi.java
    │  │          │      FooApiController.java
    │  │          │
    │  │          ├─configuration
    │  │          │      HomeController.java
    │  │          │      SpringDocConfiguration.java
    │  │          │
    │  │          └─model
    │  │                  FooIdPost200Response.java
    │  │                  FooIdPostRequest.java
    │  │
    │  └─resources
    │          application.properties
    │          openapi.yaml
    │
    └─test
        └─java
            └─org
                └─openapitools
                        OpenApiGeneratorApplicationTests.java
```

- `OpenApiGeneratorApplication` は Spinrg Boot の起動用クラスになっていて、すぐにサーバーを起動できる構成になっている
- `api` パッケージの下に、`paths` で定義した API に対応する Spring Web MVC のコントローラが生成されている
- 以下が、実際に出力されたソース（見やすくするため改行はいじっている）

```java:FooApi.java
/**
 * NOTE: This class is auto generated by OpenAPI Generator
 * (https://openapi-generator.tech) (7.13.0).
 * https://openapi-generator.tech
 * Do not edit the class manually.
 */
package org.openapitools.api;

import org.openapitools.model.FooIdPost200Response;
import org.openapitools.model.FooIdPostRequest;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Generated;

@Generated(
    value = "org.openapitools.codegen.languages.SpringCodegen",
    date = "2025-05-31T22:02:11.662270100+09:00[Asia/Tokyo]",
    comments = "Generator version: 7.13.0"
)
@Validated
@Tag(name = "foo", description = "the foo API")
public interface FooApi {

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * POST /foo/{id}
     *
     * @param id  (required)
     * @param fooIdPostRequest  (optional)
     * @return 200 OK (status code 200)
     */
    @Operation(
        operationId = "fooIdPost",
        responses = {
            @ApiResponse(
                responseCode = "200",
                description = "200 OK",
                content = {
                    @Content(
                        mediaType = "application/json",
                        schema = @Schema(implementation = FooIdPost200Response.class)
                    )
                }
            )
        }
    )
    @RequestMapping(
        method = RequestMethod.POST,
        value = "/foo/{id}",
        produces = { "application/json" },
        consumes = { "application/json" }
    )
    
    default ResponseEntity<FooIdPost200Response> fooIdPost(
        @Min(1)
        @Max(9999)
        @Parameter(
            name = "id",
            description = "",
            required = true,
            in = ParameterIn.PATH
        )
        @PathVariable("id")
        Integer id,
        
        @Parameter(name = "FooIdPostRequest", description = "")
        @Valid
        @RequestBody(required = false)
        FooIdPostRequest fooIdPostRequest
    ) {
        getRequest().ifPresent(request -> {
            for (MediaType mediaType
                    : MediaType.parseMediaTypes(request.getHeader("Accept"))) {
                if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                    String exampleString = "{ \"message\" : \"message\" }";
                    ApiUtil.setExampleResponse(request, "application/json", exampleString);
                    break;
                }
            }
        });
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
    }
}
```

```java:FooApiController.java
package org.openapitools.api;

import org.openapitools.model.FooIdPost200Response;
import org.openapitools.model.FooIdPostRequest;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.context.request.NativeWebRequest;

import javax.validation.constraints.*;
import javax.validation.Valid;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Generated;

@Generated(
    value = "org.openapitools.codegen.languages.SpringCodegen",
    date = "2025-05-31T22:02:11.662270100+09:00[Asia/Tokyo]",
    comments = "Generator version: 7.13.0"
)
@Controller
@RequestMapping("${openapi.aPITitle.base-path:}")
public class FooApiController implements FooApi {

    private final NativeWebRequest request;

    @Autowired
    public FooApiController(NativeWebRequest request) {
        this.request = request;
    }

    @Override
    public Optional<NativeWebRequest> getRequest() {
        return Optional.ofNullable(request);
    }

}
```

- API の定義に関する情報は、基本的に `FooApi` のインタフェースの方に設定されている


#### その他オプション

- spring のオプション全量は [こちら](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/spring.md)
- この中から、個人的に使うことになりそうだなぁと思ったものをピックアップする

|オプション|デフォルト値|説明|
|---|---|---|
|`apiPackage`|`org.openapitools.api`|APIクラスの出力先パッケージ|
|`basePackage`|`org.openapitools`|ベースのパッケージ|
|`configPackage`|`org.openapitools.configuration`|設定クラスの出力先パッケージ|
|`dateLibrary`|`legacy`|日付クラスに何を使うか。<br>**joda**: Joda<br>**legacy**: `java.util.Date`<br>**java8-localtime**: `LocalDateTime`<br>**java8**: JSR310のクラス|
|`generateBuilders`|`false`|モデルクラスのビルダーを生成するかどうか|
|`modelPackage`|`org.openapitools.model`|モデルクラスの出力先パッケージ|
|`interfaceOnly`|`false`|APIのインタフェースのみを生成するか|
|`useBeanValidation`|`true`|Bean Validationを使用するかどうか|

### Maven で生成する

- ここまではコマンドラインツールで生成していたが、実際は Maven や Gradle など、ビルドツールのプラグインを導入してソースは自動生成することが多いとおもう
- ここでは Maven のプラグインでソースを自動生成させてみる
- OpenAPI Generator の Maven プラグインとしては [openapi-generator-maven-plugin](https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-maven-plugin/README.md) というものが用意されている
- 以下のようなプロジェクトを用意して生成させてみる

```text:フォルダ構成
|-pom.xml
`-src/main/
  `-resources/
    `-api.yml
```

```xml: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>sandbox</groupId>
  <artifactId>openapi-generator</artifactId>
  <version>1.0.0</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
  </properties>

  <dependencies>
    <!-- とりあえずコンパイルを通すのに必要だった分だけを記載 -->
    <dependency>
      <groupId>jakarta.ws.rs</groupId>
      <artifactId>jakarta.ws.rs-api</artifactId>
      <version>2.1.6</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.jaxrs</groupId>
      <artifactId>jackson-jaxrs-json-provider</artifactId>
      <version>2.17.1</version>
    </dependency>
    <dependency>
      <groupId>javax.annotation</groupId>
      <artifactId>javax.annotation-api</artifactId>
      <version>1.3.2</version>
    </dependency>
    <dependency>
      <groupId>io.swagger</groupId>
      <artifactId>swagger-annotations</artifactId>
      <scope>provided</scope>
      <version>1.5.3</version>
    </dependency>
    <dependency>
      <groupId>jakarta.validation</groupId>
      <artifactId>jakarta.validation-api</artifactId>
      <version>2.0.2</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.openapitools</groupId>
        <artifactId>openapi-generator-maven-plugin</artifactId>
        <version>7.13.0</version>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
            <configuration>
              <!-- 入力となる API 定義 -->
              <inputSpec>src/main/resources/api.yml</inputSpec>
              <!-- 使用するジェネレータ -->
              <generatorName>jaxrs-spec</generatorName>
              
              <!-- ジェネレータの設定 -->
              <configOptions>
                <!--
                  出力先ディレクトリ.
                  デフォルト値でいいので指定しておくとソースディレクトリとして認識される.
                -->
                <sourceFolder>src/gen/java</sourceFolder>
              </configOptions>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
```

```yaml:api.yml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /foo/{id}:
    post:
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 9999
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                value:
                  type: string
                  pattern: ^[a-zA-Z0-9]+$
      responses:
        "200":
          description: 200 OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
```

- 以下のコマンドでソースが自動生成される

```bat
mvn clean compile
```

- 以下のような感じで生成される

```text:一部のみ記載
|-pom.xml
|-src/
| :
`-target/
  |-generated-sources/
  : |-openapi/
    : |-pom.xml
      |-README.md
      `-src/
        |-main/
        | :
        `-gen/
          `-java/
            `-org/openapitools/
              |-api/
              `-model/
```

#### 説明
```xml:pom.xml
      <plugin>
        <groupId>org.openapitools</groupId>
        <artifactId>openapi-generator-maven-plugin</artifactId>
        <version>7.13.0</version>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
            <configuration>
              <!-- 入力となる API 定義 -->
              <inputSpec>src/main/resources/api.yml</inputSpec>
              <!-- 使用するジェネレータ -->
              <generatorName>jaxrs-spec</generatorName>
              
              <!-- ジェネレータの設定 -->
              <configOptions>
                <!--
                  出力先ディレクトリ.
                  デフォルト値でいいので指定しておくとソースディレクトリとして認識される.
                -->
                <sourceFolder>src/gen/java</sourceFolder>
              </configOptions>
            </configuration>
          </execution>
        </executions>
      </plugin>
```

- ソースの生成は、 `openapi-generator-maven-plugin` の `generate` ゴールを使用する
    - デフォルトで `generate-resources` フェーズに紐づくようになっている
- `configuration` で各種設定をしていく
    - `inputSpec` で、入力となる API 定義ファイルを指定する
    - `generatorName` で、使用するジェネレータを指定する
    - `configOptions` でジェネレータの設定を指定できる
        - `sourceFolder` はジェネレータが出力するソースコードのディレクトリパスを指定するためのオプション
        - jaxrs-spec ジェネレータのデフォルト値は `src/gen/java`
        - このオプションをデフォルト値でいいので設定しておくと、自動的に出力先のディレクトリがソースディレクトリとして認識されるようになる
            - (デフォルト値があるのに未指定だと認識されないのがなんか気持ち悪い)
    - そのほかの設定については [公式ドキュメント](https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-maven-plugin/README.md#general-configuration-parameters) を参照

```text:出力ファイル
|-pom.xml
|-src/
| :
`-target/
  |-generated-sources/
  : |-openapi/
    : |-pom.xml
      |-README.md
      `-src/
        |-main/
        | :
        `-gen/
          `-java/
            `-org/openapitools/
              |-api/
              `-model/
```

- `generate` ゴールの結果は `target/generated-sources` の下に出力される

```xml:pom.xml
  <dependencies>
    <!-- とりあえずコンパイルを通すのに必要だった分だけを記載 -->
    <dependency>
      <groupId>jakarta.ws.rs</groupId>
      <artifactId>jakarta.ws.rs-api</artifactId>
      <version>2.1.6</version>
      <scope>provided</scope>
    </dependency>
    ...
  </dependencies>
```

- 最後に、自動生成されたソースコードのビルドが通るように依存関係を追加している
- この依存関係は、自動生成されたファイル類の中に含まれる pom.xml (`target/generated-sources/openapi/pom.xml`) に記載されているものを持ってきて、コンパイルが通る必要最小限になるまで削ったものになる
    - API定義の内容次第で必要な依存関係は増減すると思うので、実際のプロジェクトごとに要調整

#### ジェネレータの設定ファイルを指定する

```text:フォルダ構成
|-pom.xml
`-src/main/resources/
  |-api.yml
  `-openapi-generator-conf.yml
```

```yaml:openapi-generator-conf.yml
interfaceOnly: true
```

```xml:pom.xml
      <plugin>
        <groupId>org.openapitools</groupId>
        <artifactId>openapi-generator-maven-plugin</artifactId>
        <version>7.13.0</version>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
            <configuration>
              ...
              <!-- ジェネレータの設定ファイル -->
              <configurationFile>
                ${project.basedir}/src/main/resources/openapi-generator-conf.yml
              </configurationFile>
            </configuration>
          </execution>
        </executions>
      </plugin>
```

- ジェネレータの設定ファイルは `configurationFile` で指定できる


## クライアントサイド
### Hello World
```yaml:sample.yml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /foo:
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        "201":
          description: 201 Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
  /foo/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: 200 OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
        "404":
          description: 404 Not Found
```


```bash:コマンド
java -jar openapi-generator-cli-7.13.0.jar generate \
    -i sample.yml \
    -o output \
    -g java
```

- Java のクライアントサイドのソースを生成する場合は、 [java](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/java.md) ジェネレータを使用する
- 以下が、実際に出力されたファイルたち

```text:出力結果
│  .gitignore
│  .openapi-generator-ignore
│  .travis.yml
│  build.gradle
│  build.sbt
│  git_push.sh
│  gradle.properties
│  gradlew
│  gradlew.bat
│  pom.xml
│  README.md
│  settings.gradle
│
├─.github
│  └─workflows
│          maven.yml
│
├─.openapi-generator
│      FILES
│      VERSION
│
├─api
│      openapi.yaml
│
├─docs
│      DefaultApi.md
│      FooIdGet200Response.md
│      FooPost201Response.md
│      FooPostRequest.md
│
├─gradle
│  └─wrapper
│          gradle-wrapper.jar
│          gradle-wrapper.properties
│
└─src
    ├─main
    │  │  AndroidManifest.xml
    │  │
    │  └─java
    │      └─org
    │          └─openapitools
    │              └─client
    │                  │  ApiCallback.java
    │                  │  ApiClient.java
    │                  │  ApiException.java
    │                  │  ApiResponse.java
    │                  │  Configuration.java
    │                  │  GzipRequestInterceptor.java
    │                  │  JSON.java
    │                  │  Pair.java
    │                  │  ProgressRequestBody.java
    │                  │  ProgressResponseBody.java
    │                  │  ServerConfiguration.java
    │                  │  ServerVariable.java
    │                  │  StringUtil.java
    │                  │
    │                  ├─api
    │                  │      DefaultApi.java
    │                  │
    │                  ├─auth
    │                  │      ApiKeyAuth.java
    │                  │      Authentication.java
    │                  │      HttpBasicAuth.java
    │                  │      HttpBearerAuth.java
    │                  │
    │                  └─model
    │                          AbstractOpenApiSchema.java
    │                          FooIdGet200Response.java
    │                          FooPost201Response.java
    │                          FooPostRequest.java
    │
    └─test
        └─java
            └─org
                └─openapitools
                    └─client
                        ├─api
                        │      DefaultApiTest.java
                        │
                        └─model
                                FooIdGet200ResponseTest.java
                                FooPost201ResponseTest.java
                                FooPostRequestTest.java
```

- なにやら色々出力されている
- 出力されたプロジェクトのルートに移動し、下記コマンドを実行してローカルリポジトリにインストールする

```bash:ローカルリポジトリにインストール
mvn install
```

- 続いて、全く別の Maven プロジェクトを作成する

```bash:新規Mavenプロジェクト作成
mvn archetype:generate \
    -DarchetypeGroupId=org.apache.maven.archetypes \
    -DarchetypeArtifactId=maven-archetype-simple \
    -DarchetypeVersion=1.5
...
Define value for property 'groupId': sandbox
Define value for property 'artifactId': openapi-client
Define value for property 'version' 1.0-SNAPSHOT: 1.0.0
Define value for property 'package' sandbox:
...
```

- 生成されたプロジェクトの `pom.xml` を開き、 `dependency` に以下を追加
    - これは、先ほど `mvn install` でローカルリポジトリにインストールしたクライアントモジュールを指している

```xml:pom.xml
    <dependency>
      <groupId>org.openapitools</groupId>
      <artifactId>openapi-java-client</artifactId>
      <version>1.0</version>
    </dependency>
```

- `src/main/java/sandbox/App.java` を開き、以下のように実装する

```java:App.java
package sandbox;

import org.openapitools.client.ApiClient;
import org.openapitools.client.ApiException;
import org.openapitools.client.Configuration;
import org.openapitools.client.api.DefaultApi;
import org.openapitools.client.model.FooIdGet200Response;

/**
 * Hello world!
 *
 */
public class App
{
    public static void main( String[] args )
    {
        ApiClient defaultClient = Configuration.getDefaultApiClient();
        defaultClient.setBasePath("http://localhost:4010");

        DefaultApi apiInstance = new DefaultApi(defaultClient);
        Integer id = 56; // Integer |
        try {
            FooIdGet200Response result = apiInstance.fooIdGet(id);
            System.out.println(result);
        } catch (ApiException e) {
            System.err.println("Exception when calling DefaultApi#fooIdGet");
            System.err.println("Status code: " + e.getCode());
            System.err.println("Reason: " + e.getResponseBody());
            System.err.println("Response headers: " + e.getResponseHeaders());
            e.printStackTrace();
        }
    }
}
```

- Prism でモックサーバーを立ち上げる
    - モックサーバーについては [こちら](#モックサーバー) を参照

```bash
docker run --rm --init \
    -v `cd`/sample.yml:/tmp/sample.yml \
    -p 4010:4010 \
    stoplight/prism:4 mock -h 0.0.0.0 "/tmp/sample.yml"
```

- 先ほどの `App.java` を実行してみる

```text:実行結果
class FooIdGet200Response {
    id: 0
    name: string
}
```


### 説明
#### 利用方法
- java ジェネレータで出力されるソースコードは、インプットとなったAPI定義で宣言されたAPIを実行するための、独立したクライアントライブラリとなっている
- 既存のプロジェクトの中に自動生成したソースコードを取り込んで利用するの**ではなく**、この出力したプロジェクトを一旦ビルドして jar にして、依存ライブラリとして他のプロジェクトから利用する、という形態を想定しているっぽい
    - ソースを組み込んで使う方法も調整次第でできるとは思う

#### ドキュメント
- 出力されたファイルたちのルートに `README.md` があり、ここにプロジェクトのビルド方法やその後の使い方が詳細に書かれている
- クライアントライブラリとしての利用方法も描かれており、個々の API の詳細へのリンクも記載されている

```markdown:README.md(一部)
...

## Documentation for API Endpoints

All URIs are relative to *http://localhost*

Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
*DefaultApi* | [**fooIdGet**](docs/DefaultApi.md#fooIdGet) | **GET** /foo/{id} | 
*DefaultApi* | [**fooPost**](docs/DefaultApi.md#fooPost) | **POST** /foo | 
...
```

- 個々の API は、 `docs` の下に出力された Markdown へリンクされており、そちらにさらに詳細が出力されている

```markdown:docs/DefaultApi.md(一部)
...
### Parameters

| Name | Type | Description  | Notes |
|------------- | ------------- | ------------- | -------------|
| **id** | **Integer**|  | |

### Return type

[**FooIdGet200Response**](FooIdGet200Response.md)
...
```


#### 使い方
```java
        ApiClient defaultClient = Configuration.getDefaultApiClient();
        defaultClient.setBasePath("http://localhost:4010");

        DefaultApi apiInstance = new DefaultApi(defaultClient);
        Integer id = 56; // Integer |
        try {
            FooIdGet200Response result = apiInstance.fooIdGet(id);
```

- 自動生成されたクラスには `*Api` という名前のクラスが出力されている
    - 仮に、ここでは**APIクラス**と呼称する
    - API クラスはタグごとに出力され、 `*` の部分にはタグ名が割り当てられる
    - ここではタグを設定していなかったので、デフォルトタグである `Default` がついたクラスが出力されている
- このAPIクラスに、API定義で宣言したAPIを実行するためのメソッドが用意されている
- APIクラスは普通にコンストラクタでインスタンスを生成するが、このとき引数で `ApiClient` のインスタンスを渡す必要がある
- この `ApiClient` は、実際に HTTP 通信を実行するためのクラスで、裏で使用する HTTP クライアントライブラリをラップする存在となっている
    - なお、デフォルトでは java ジェネレータは OkHttp を使用する実装となっている
- `ApiClient` のインスタンスは、 `Configuration` の `getDefaultApiClient` で取得できる
- `Configuration.getDefaultApiClient` の実装を見に行くと、以下のようになっている

```java:Configuration
  private static final AtomicReference<ApiClient> defaultApiClient
      = new AtomicReference<>();
  ...
  public static ApiClient getDefaultApiClient() {
    ApiClient client = defaultApiClient.get();
    if (client == null) {
      client = defaultApiClient.updateAndGet(val -> {
        if (val != null) { // changed by another thread
          return val;
        }
        return apiClientFactory.get();
      });
    }
    return client;
  }
```

- `AtomicReference` を使ってインスタンスがキャッシュされており、2回目以降は同じインスタンスを返すようになっている
- 各クラスの関係を整理すると、以下のような感じ

```plantuml
class "APIクラス" as api
class Configuration
class ApiClient
class "HTTPクライアントライブラリ" as lib
class "アプリケーション" as app

app --> api: API呼び出し
api --> ApiClient: HTTP処理委譲
ApiClient --> lib: HTTP通信実行
ApiClient <- Configuration: 生成・キャッシュ
```

### 利用するHTTPクライアントライブラリとJSONライブラリを変更する
- デフォルトでは OkHttp と Gsonを利用するが、設定によって様々なライブラリに切り替えることができる
- 切り替えは、 java ジェネレータの `library` オプションでできる
- 指定できる値は以下(v7.13.0時点)

|設定値|HTTPクライアントライブラリ|JSON処理|
|---|---|---|
|`jersey2`|Jersey client 2.25.1|Jackson 2.17.1|
|`jersey3`|Jersey client 3.1.1|Jackson 2.17.1|
|`feign`|OpenFeign 13.2.1|Jackson 2.17.1 or <br>Gson 2.10.1|
|`feign-hc5`|OpenFeign 13.2.1/HttpClient5 5.4.2|Jackson 2.17.1 or <br>Gson 2.10.1|
|`okhttp-gson`|OkHttp 4.11.0|Gson 2.10.1|
|`retrofit2`|OkHttp 4.11.0|Gson 2.10.1 (Retrofit 2.5.0) or <br>Jackson 2.17.1|
|`resttemplate`|Spring RestTemplate 5.3.33<br>(`useJakartaEe` が `true` の場合は 6.1.5)|Jackson 2.17.1|
|`webclient`|Spring WebClient 5.1.18|Jackson 2.17.1|
|`restclient`|Spring RestClient 6.1.6|Jackson 2.17.1|
|`resteasy`|Resteasy client 4.7.6|Jackson 2.17.1|
|`vertx`|VertX client 3.5.2.|Jackson 2.17.1|
|`google-api-client`|Google API client 2.2.0|Jackson 2.17.1|
|`rest-assured`|rest-assured 5.3.2|Gson 2.10.1 or <br>Jackson 2.17.1|
|`native`|Java native HttpClient|Jackson 2.17.1|
|`microprofile`|Microprofile client 2.0|JSON-B 1.0.2 or <br>Jackson 2.17.1|
|`apache-httpclient`|Apache httpclient 5.2.1|Jackson 2.17.1|

- デフォルトは `okhttp-gson` となる


# API 定義ファイルを分割する

- API 定義のファイルが肥大化してくると、分割して管理したくなってくる
- 各ツールで処理できるようにファイルを分割する方法について整理する
- ここでの分割方法は自分なりに色々試した結果なので、ベストプラクティスかどうかはわからない

## 大まかな基本方針・前提

- ファイル編集は VS Code を使用する
    - 拡張機能として [OpenAPI (Swagger) Editor](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi) を入れている
- API ドキュメントは、 Swagger UI で表示できるようにする
- コード生成は OpenAPI Generator の Maven プラグインで実行できるようにする
- 基本は分割したままの管理だが、その他使用するツールが単一ファイルしかサポートしていない場合も想定して、結合する方法についても調べておく
    - 自分が知っているものとしては、例えば Azure の API Management で API を OpenAPI の API 定義ファイルからロードすることができるが、その際は単一のファイルでないと読み込ませられない（はず）

## 実装
- 細かい説明はおいおいしていくとして、まずはざっとファイル分割した場合の構成と中身を記載する

```text:ファイル構成
openapi/
|-api.yml
|-components/
| `-book.yml
`-paths/
  `-books.yml
```

```yaml:api.yml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  # Books API
  /books:
    $ref: "./paths/books.yml#/paths/~1books"
  /books/{id}:
    $ref: "./paths/books.yml#/paths/~1books~1%7Bid%7D"
```

```yaml:components/book.yml
openapi: '3.1.1'
info:
  title: Book components
  version: '1.0'
components:
  schemas:
    BookId:
      type: integer
    BookTitle:
      type: string
    BookPrice:
      type: integer
    Book:
      type: object
      properties:
        id:
          $ref: "#/components/schemas/BookId"
        title:
          $ref: "#/components/schemas/BookTitle"
        price:
          $ref: "#/components/schemas/BookPrice"
```

```yaml:paths/books.yml
openapi: '3.1.1'
info:
  title: Books API
  version: '1.0'
paths:
  /books:
    get:
      responses:
        "200":
          description: 200 OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "../components/book.yml#/components/schemas/Book"
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  $ref: "../components/book.yml#/components/schemas/BookTitle"
                price:
                  $ref: "../components/book.yml#/components/schemas/BookPrice"
      responses:
        "201":
          description: success
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    $ref: "../components/book.yml#/components/schemas/BookId"
  /books/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: "../components/book.yml#/components/schemas/BookId"
      responses:
        "200":
          description: 200 OK
          content:
            application/json:
              schema:
                $ref: "../components/book.yml#/components/schemas/Book"
```

- ありがちな、全データ取得、登録、1件取得の３種類のエンドポイントを定義している

### 説明
#### ファイル構成
```text:ファイル構成
openapi/
|-api.yml
|-components/
| `-book.yml
`-paths/
  `-books.yml
```

- ルートとなるファイルと、分割したファイルで構成している
    - 分割の方法については色々考えられるだろうが、ここではとりあえず `paths` や `components` など、 [OpenAPI Object](https://spec.openapis.org/oas/latest.html#openapi-object) で定義されたプロパティ単位でフォルダを切ってみている
    - `paths` については「`/books` で始まる API」のようにリソースのまとまりでファイルを分けてみている
    - `components` については、ひとまずリソース単位で分ける感じにしてみている
    - あくまで例の１つなので、適宜カスタマイズすればいい

#### ルートファイル
```yaml:api.yml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  # Books API
  /books:
    $ref: "./paths/books.yml#/paths/~1books"
  /books/{id}:
    $ref: "./paths/books.yml#/paths/~1books~1%7Bid%7D"
```

- ルートとなるファイルは Swagger UI や OpenAPI Generator などのツールにパラメータとして渡すファイルになる
- このファイルには `paths` を全て記述することになる
- ただし、 `paths` の定義で記述するのはキーとなるパスのみで、実体は `$ref` を使って分割したファイル内の定義を参照する形にしている
- ここで、いくつか特殊な記法を用いているのでそれぞれ解説する

##### スラッシュのエスケープ
```yaml
  /books:
    $ref: "./paths/books.yml#/paths/~1books"
```

- `$ref` の中に `~1` という見慣れない表記がある
- これはスラッシュ (`/` ) のエスケープで、 JSON Pointer の仕様に従っている
- つまり、 `~1books` は `/books` を表している
- ちなみにチルダ (`~`) がパスに含まれる場合は `~0` で記述できる

##### 波括弧のエスケープ
```yaml
  /books/{id}:
    $ref: "./paths/books.yml#/paths/~1books~1%7Bid%7D"
```

- この `%7Bid%7D` の部分はパーセントエンコードになっていて、 `{id}` を意味している
- これ自体は本来の OpenAPI の仕様的には必要ないが、 OpenAPI Generator のバグに対応するため仕方なくこう記載している
- `{id}` のままだと OpenAPI Generator でソースを生成しようとしたときにエラーが発生する
- これを回避するために、仕方なくパーセントエンコードをしている
- 詳しくは以下の Issue を参照
    - [[BUG] Error resolving complex Reference with path parameter #21058](https://github.com/OpenAPITools/openapi-generator/issues/21058)
    - 一応こちらの Issue では Maven を使って実行時にパーセントエンコードへ切り替える方法も紹介されている

#### 各分割ファイル
```yaml:components/book.yml
openapi: '3.1.1'
info:
  title: Book components
  version: '1.0'
components:
  schemas:
    BookId:
      type: integer
    BookTitle:
      type: string
    BookPrice:
      type: integer
    Book:
      type: object
      properties:
        id:
          $ref: "#/components/schemas/BookId"
        title:
          $ref: "#/components/schemas/BookTitle"
        price:
          $ref: "#/components/schemas/BookPrice"
```

- 各分割ファイルには本来 `openapi` や `info` は必要ないが、あえて記載するようにしている
- その理由は、 VS Code で編集したときに補完を効かせるため
    - これらが無いとただの YAML ファイル判定になって OpenAPI としての補完が効かないので
    - なので `title` や `version` は正直適当でいい（実際、これらは使われないので）
- `components` をどこまで細かく定義すべきかは議論の余地はあると思うが、いったんここでは一番細かい単位まで定義してみた
    - 共通化を考えたら、個人的には結局はこの細かさに行きつく気はするが

#### API定義

```yaml:paths/books.yml
openapi: '3.1.1'
info:
  title: Books API
  version: '1.0'
paths:
  /books:
    get:
      responses:
        "200":
          description: 200 OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "../components/book.yml#/components/schemas/Book"
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  $ref: "../components/book.yml#/components/schemas/BookTitle"
                price:
                  $ref: "../components/book.yml#/components/schemas/BookPrice"
      ...
```

- こちらも VS Code で修正するときに補完が効くように `openapi` を設定している
- 各 API 定義からは、 `components` で定義した `schema` を参照する形で利用している


## 各ツールの実行結果
### Swagger UI
```text:フォルダ構成
tomcat/webapps/swagger/
 |-swagger-initializer.js
 |-openapi/
 : |-api.yml
   |-components/
   `-paths/
```

- 先ほどのファイル類を Swagger UI をデプロイしているところと同じ場所に配置する

```js:swagger-initialzier.js
...
  window.ui = SwaggerUIBundle({
    url: "openapi/api.yml",
    ...
  });

  //</editor-fold>
};
```

- `swagger-initializer.js` の url に、ルートとなる API 定義ファイルへのパスを設定する

**実行結果**

![localhost_8080_swagger__urls.primaryName=API.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/3cb6fd90-bde7-43d4-94a4-c523b7a3f386.png)

- ちゃんと分割したファイルの内容も反映された形でドキュメントが生成できている

### OpenAPI Generator (Maven プラグイン)
```text:フォルダ構成
|-pom.xml
`-src/main/
  |-java/
  `-resources/
    `-openapi/
      |-api.yml
      |-components/
      `-paths/
```

- `src/main/resources/openapi` の下に API 定義ファイルを配置している

```xml:pom.xml
      <plugin>
        <groupId>org.openapitools</groupId>
        <artifactId>openapi-generator-maven-plugin</artifactId>
        <version>7.13.0</version>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
            <configuration>
              <inputSpec>src/main/resources/openapi/api.yml</inputSpec>
              <generatorName>jaxrs-spec</generatorName>
              
              <configOptions>
                <sourceFolder>src/gen/java</sourceFolder>
              </configOptions>
            </configuration>
          </execution>
        </executions>
      </plugin>
```

- `inputSpec` でルートとなる API 定義ファイルのパスを指定する

:::note warn
`inputSpec` を `${project.basedir}/src/main/resources/...` のようにすると、少なくとも Windows 環境では以下のようなエラーになるので注意。

```log:エラーログ
[ERROR] Error resolving ./paths/books.yml#/paths/~1books~1%7Bid%7D
java.net.URISyntaxException: Illegal character in opaque part at index 2: C:\sandbox\openapi\generator\openapi-generator/src/main/resources/openapi/api.yml
    at java.net.URI$Parser.fail (URI.java:2995)
    at java.net.URI$Parser.checkChars (URI.java:3166)
    at java.net.URI$Parser.parse (URI.java:3202)
    at java.net.URI.<init> (URI.java:645)
```

`${project.basedir}` の値が Windows 環境だと `C:\xxx` の形式に展開されるが、その値が `URI` クラスへ渡されたときに構文エラーとなってしまう。
:::

### VS Code で編集したとき
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28302/1e750d67-3346-4b64-b334-cdbfafb39977.png)

- `$ref` を書くときにパスを補完してくれるので捗る

## ファイルを連結させる
- 一応これまでの方法なら Swagger UI によるドキュメント生成も OpenAPI Generator によるソース生成もファイルを分割したまま実施できる
- ただ、ツールによってはファイルが１つにまとまっていないといけないケースも、なくはないかもしれない
- そこで、これらのファイルを単一のファイルに連結する方法について記載する

### Swagger CLI と Redocly CLI
- 分割された OpenAPI の API 定義ファイルを１つにまとめるツールとして、かつては [Swagger CLI](https://github.com/APIDevTools/swagger-cli) というのが存在した
- ただ、現在は開発が止まっていて GitHub リポジトリもアーカイブされている
- このリポジトリの冒頭でも説明されているが、現在は [Redocly CLI](https://redocly.com/redocly-cli) への移行が推奨されている
- ということで、この Redocly CLI で API 定義ファイルを連結させる
- Redocly CLI は Node のアプリなので npm でインストールできる

```text:redocly-cliのインストール
npm i -g @redocly/cli@latest
```

- [Docker イメージ](https://hub.docker.com/r/redocly/cli)も用意されているので、そちらでも可
- 今回は Docker でやってみる
- `api.yml` があるディレクトリまで移動して、以下のコマンドを実行する

```text:Dockerを使う場合
docker run --rm -v `pwd`:/spec redocly/cli bundle /spec/api.yml -o /spec/result.yml
```

- `bundle` コマンドの引数にエントリとなるルートファイルを指定して、 `-o` で出力先ファイルを指定している
- 以下が、実際に出力されたファイル

```yaml:result.yml
openapi: 3.1.1
info:
  title: API Title
  version: '1.0'
paths:
  /books:
    get:
      responses:
        '200':
          description: 200 OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Book'
    post:
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  $ref: '#/components/schemas/BookTitle'
                price:
                  $ref: '#/components/schemas/BookPrice'
      responses:
        '201':
          description: success
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    $ref: '#/components/schemas/BookId'
  /books/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/BookId'
      responses:
        '200':
          description: 200 OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Book'
components:
  schemas:
    BookId:
      type: integer
    BookTitle:
      type: string
    BookPrice:
      type: integer
    Book:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/BookId'
        title:
          $ref: '#/components/schemas/BookTitle'
        price:
          $ref: '#/components/schemas/BookPrice'
```

- `$ref` の参照が単一ファイル構成用に書き直されていて、いい感じに連結されている


# json-schema-validator

- OpenAPI Generator で生成されるソースコードには、 Bean Validation などである程度バリデーション処理が反映される
- ただ、 JSON Schema で定義できる全ての制約をチェックできるかというそうでもない
- 餅は餅屋ということで、 JSON Schema で定義した制約を満たせているかどうかは専用のライブラリでチェックしてみる
- 使用するのは [json-schema-validator](https://github.com/networknt/json-schema-validator) というライブラリ
- **OpenAPI Support** というのがあって、 OpenAPI の API 定義ファイルをそのままの形で読み込んでバリデーションに利用できる（Schema 部分だけを抽出とかしなくていい）

## 実装
```ファイル構成
|-build.gradle
`-src/main/
  |-java/
  | `-sandbox/jsonschema/
  |   `-Main.java
  `-resources/
    `-schema/
      `-sample.yml
```

```gradle:build.gradle
plugins {
    id "java"
}

sourceCompatibility = 21
targetCompatibility = 21

compileJava.options*.encoding = "UTF-8"

repositories {
    mavenCentral()
}

dependencies {
    implementation "com.networknt:json-schema-validator:1.5.7"
}
```

```yaml:sample.yml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HogePostRequestBody"
      responses:
        "200":
          description: 200 OK
components:
  schemas:
    HogePostRequestBody:
      type: array
      prefixItems:
        - type: number
        - type: string
      items:
        type: integer
```

```java:Main.java
package sandbox.jsonschema;

import com.networknt.schema.InputFormat;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SchemaLocation;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import com.networknt.schema.oas.OpenApi31;

import java.util.Set;

public class Main {
    public static void main(String[] args) {
        JsonSchemaFactory factory = 
            JsonSchemaFactory.getInstance(
                SpecVersion.VersionFlag.V202012,
                builder -> builder.metaSchema(OpenApi31.getInstance())
                                  .defaultMetaSchemaIri(OpenApi31.getInstance().getIri())
            );

        JsonSchema schema =
            factory.getSchema(SchemaLocation.of(
                "classpath:schema/sample.yml#/components/schemas/HogePostRequestBody"
            ));

        test(schema, """
        [10.12, "Hello", 191, 46]""");

        test(schema, """
        [10, false, "foo", 21.2]""");
    }

    private static void test(JsonSchema schema, String json) {
        System.out.println("==========");
        System.out.println("json = " + json);
        
        Set<ValidationMessage> messages = schema.validate(json, InputFormat.JSON);
        if (messages.isEmpty()) {
            System.out.println("result: valid");
        } else {
            System.out.println("result: invalid");
            System.out.println("messages:");
            for (ValidationMessage message : messages) {
                System.out.println(message.getMessage());
            }
        }
        System.out.println("==========\n");
    }
}
```

```text:実行結果
==========
json = [10.12, "Hello", 191, 46]
result: valid
==========

==========
json = [10, false, "foo", 21.2]
result: invalid
messages:
$[1]: boolean found, string expected
$[2]: string found, integer expected
$[3]: number found, integer expected
==========
```

## 説明
```gradle:build.gradle
    implementation "com.networknt:json-schema-validator:1.5.7"
```

- [com.networknt:json-schema-validator](https://central.sonatype.com/artifact/com.networknt/json-schema-validator) を依存関係に追加する

```java:Main.java
        JsonSchemaFactory factory = 
            JsonSchemaFactory.getInstance(
                SpecVersion.VersionFlag.V202012,
                builder -> builder.metaSchema(OpenApi31.getInstance())
                                  .defaultMetaSchemaIri(OpenApi31.getInstance().getIri())
            );
```

- まずは `JsonSchemaFactory` を生成する
- このあたりの実装は[公式ドキュメントの記述](https://github.com/networknt/json-schema-validator/blob/master/doc/openapi.md)をそのまま持ってきてるだけ

```java:Main.java
        JsonSchema schema =
            factory.getSchema(SchemaLocation.of(
                "classpath:schema/sample.yml#/components/schemas/HogePostRequestBody"
            ));
```

- 次に検証で使用したい Schema 定義を読み込んで `JsonSchema` オブジェクトを取得する
- このとき、 `SchemaLocation.of` で JSON Pointer を利用した記法で読み込みたい Schema 定義を指定する
    - スラッシュは `~1` でエスケープが必要なので注意
- クラスパス上のファイルを指定する場合は `classpath:` で始める
    - ローカルファイルの場合は `file:///` で始める

```java:Main.java
        final Set<ValidationMessage> messages = schema.validate(json, InputFormat.JSON);
        if (messages.isEmpty()) {
            System.out.println("result: valid");
        } else {
            System.out.println("result: invalid");
            System.out.println("messages:");
            for (ValidationMessage message : messages) {
                System.out.println(message.getMessage());
            }
        }
```

- `JsonSchema` の `validate` メソッドで検証を実行できる
- 結果は `ValidationMessage` の `Set` で返される
- 問題ない場合は空が返り、問題がある場合は違反内容ごとに `ValidationMessage` が設定されている
- なお、試した限りでは YAML が分割されていても、問題なく `$ref` で指定した先のファイルまで見て検証してくれた

# モックサーバー

- API 定義ファイルをもとにモックサーバーを作る方法
- モックサーバーには [Prism](https://stoplight.io/open-source/prism) というツールを使用する
- ドキュメントは [こちら](https://docs.stoplight.io/docs/prism/674b27b261c3c-prism-overview)

## 起動
- npm でインストールできるが、 [Docker イメージ](https://hub.docker.com/r/stoplight/prism/) も用意されているので Docker を使う

```bash
docker run --rm --init \
    -v `pwd`/api.yml:/tmp/api.yml \
    -p 4010:4010 \
    stoplight/prism:4 mock -h 0.0.0.0 "/tmp/api.yml"
```


## 動作確認
以下の API 定義を読み込んで起動してみる。

```yaml:api.yml
openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /foo:
    get:
      responses:
        "200":
          description: 200 OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/successResponse"
              examples:
                success1:
                  value:
                    id: 10
                    value: Get Foo example success1.
                success2:
                  value:
                    id: 20
                    value: Get Foo example success2.
        "400":
          description: 400 Bad Request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/errorResponse"
              examples:
                singleError:
                  value:
                    - message: single error message
                multipleError:
                  value:
                    - message: first error
                    - message: second error
    post:
      responses:
        "200":
          description: 200 OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/successResponse"
              examples:
                success:
                  value:
                    id: 30
                    value: Post Foo example success.
components:
  schemas:
    successResponse:
      type: object
      properties:
        id:
          type: integer
        value:
          type: string
    errorResponse:
      type: array
      items:
        type: object
        properties:
          message:
            type: string
```

- とりあえず `/foo` に GET で単純にアクセスしてみる

```bash
$ curl http://localhost:4010/foo -s -i
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-type: application/json
Content-Length: 45
Date: Tue, 03 Jun 2025 13:06:45 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":10,"value":"Get Foo example success1."}
```

- `examples` で定義した例の1つが返された

### 返却される example を指定する
```bash
$ curl http://localhost:4010/foo -s -i -H "Prefer: example=success2"
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-type: application/json
Content-Length: 45
Date: Tue, 03 Jun 2025 13:09:30 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":20,"value":"Get Foo example success2."}
```

- `Prefer` ヘッダーを追加し、 `example=<example名>` と指定すると、対応する名前の `example` が返却される

### ステータスコードを指定する
```bash
$ curl http://localhost:4010/foo -s -i -H "Prefer: code=400"
HTTP/1.1 400 Bad Request
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-type: application/json
Content-Length: 36
Date: Tue, 03 Jun 2025 13:10:33 GMT
Connection: keep-alive
Keep-Alive: timeout=5

[{"message":"single error message"}]
```

- `Prefer` で `code=<ステータスコード>` を指定すると、レスポンスのステータスコードを変更できる
- 同時に `example` を指定したい場合はカンマで区切る

```bash:codeとexampleを同時に指定する
$ curl http://localhost:4010/foo -s -i -H "Prefer: code=400, example=multipleError"
HTTP/1.1 400 Bad Request
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-type: application/json
Content-Length: 54
Date: Tue, 03 Jun 2025 13:11:40 GMT
Connection: keep-alive
Keep-Alive: timeout=5

[{"message":"first error"},{"message":"second error"}]
```




# ベストプラクティス
[Best Practices | OpenAPI Documentation](https://learn.openapis.org/best-practices.html)

ここに書かれていることを雑に整理。

## デザインファーストのアプローチ
- OpenAPI の設計には、大きく2つのアプローチがある
    - コードファースト
    - デザインファースト
- コードファーストは、まずソースコードを作成し、そこからOpenAPI定義をリバースエンジニアリングするアプローチ
- デザインファーストは、最初にOpenAPIの定義を作成し、そこからソースのスケルトンを自動生成するアプローチ
- OpenAPI Initiativeとしての推奨は、デザインファースト
- 理由は、コードの方がAPIの表現できることが多いから
    - コードで色々やった後にOpenAPI定義をリバースすると、OpenAPIで表現できないものがあって死ぬってことだろうか
    - それなら初めからOpenAPIで書いた方が良い、みたいなことが書いてあるように読める
- あとは、OpenAPIの定義は正しく書けているかチェックできるツールがあるので、CIに統合することで安心して修正ができるようになるのでデザインファーストがいいとか

## 真の単一ソースを維持する
- 重複があるとダブルメンテになるから１つになるようにしようって話っぽい
- 「単一」というのは重複をなくすって意味で、ファイルを１つにしようって意味**ではない**と思う（私見）

## OpenAPI 定義はソースと同じように管理する
- OpenAPI 定義は単なるドキュメントではなく、そこからソースを生成したりドキュメントを生成したりテストを生成したりと様々なことができる
- ソースコードと同じように管理すべき

## OpenAPI 定義をユーザーにも提供する
- OpenAPI 定義を使うとクライアントコードを生成できたりするので、自動生成したドキュメントだけでなく、元のOpenAPI定義(多分yamlファイルのこと)も提供した方がいい 

## OpenAPI定義を手書きする必要は無い
- OpenAPI 定義は yaml や json で書けるので、手動で簡単に書くことができる
- とはいえ大規模になってくると大変なので、ツールとかを使って作成することも考えた方がいいとか、なんかそういう感じの話
- 例えばGUIエディターとかがあるので、それを利用するという手がある

## 大規模なAPIを記述する
### 同じ記述を繰り返さない
- 定義内に同じ記述が現れたら、コンポーネント定義で共通化する
- コンポーネント定義は他のファイルのものでも参照できるので、異なるAPI定義間で共有することもできる

### ファイルを分割する
- 大きすぎてもダメ、小さすぎてもファイル数が多くなって大変なので、その中間のいいところを見つける必要がある
- 経験則としては、同じパス階層のAPIは１つのファイルにまとめるのが良い
    - たとえば `/users` で始まる API (`/users`, `/users/{id}` など)は１つのファイルにまとめる
    - 元ページの説明だとディレクトリも分けるみたいな話が書かれているように読めるが、例がないので分からん
- ただし、使用するツールによっては大量に分割されたファイルに対応していないなどあるかもしれないので注意が必要

### タグを使って整理する
- オペレーションにはタグを付与することができ、これでドキュメント上のソート順などを整理できる

# 参考
- [What is OpenAPI? - OpenAPI Initiative](https://www.openapis.org/what-is-openapi)
- [OpenAPIとは｜Swaggerとの違いや役立つツールを紹介 | AeyeScan](https://www.aeyescan.jp/blog/openapi/)
- [OpenAPI Specification v3.1.1](https://spec.openapis.org/oas/latest.html)
- [Getting Started | OpenAPI Documentation](https://learn.openapis.org/)
- [OpenAPI Tooling](https://tools.openapis.org/)
- [Visual Studio CodeでOpenAPI (Swagger) Editorを使用｜Web制作プラスジャムのなかやすみ](https://note.com/plusjam/n/na7f9ac33dd40)
- [JSON Schema reference](https://json-schema.org/understanding-json-schema/reference)
- [OpenAPITools/openapi-generator: OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)](https://github.com/OpenAPITools/openapi-generator)