1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenAPI使い方メモ

Last updated at Posted at 2025-06-05

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というのがあるので、これをインストールする

ファイル作成

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

image.png

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

gomi.jpg

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

image.png

ざっくり一巡り

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

基本構造

# 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の定義を記述していく

パスの定義

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 を並べる
  • キーであるパスは、必ずスラッシュ (/) から始める形で記述する

オペレーションの定義

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

  • パスの下には、そのパスで使用できるオペレーションの定義(Operation Object)をHTTPメソッドごとに並べる
    • キーには HTTP メソッドを指定する

レスポンスの定義

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

  • 各 Operation Object の responses で、レスポンスを定義できる
  • レスポンスの定義は、HTTPのステータスコードごとに Response Object で記述する
    • Response Object は description が必須
    • ステータスコードは文字列で定義しなければならない
      • YAML と JSON との間で互換性を保つため、ってドキュメントに書いてある
      • This field MUST be enclosed in quotation marks (for example, “200”) for compatibility between JSON and 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

image.png

  • 各 Response Object の content で、レスポンスのコンテンツを定義できる
  • コンテンツの定義は Media Type Object でコンテンツタイプごとに定義する
  • 各コンテンツでは、 schema で具体的なレスポンスコンテンツの構造を定義できる

レスポンスヘッダーの定義

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

  • Response Objectheaders で、そのレスポンスで返すヘッダーを定義できる
  • headers は Map で指定し、キーにヘッダー名、値に Header Object を使用する

リクエストの定義

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

  • パラメータ
  • リクエストボディ(メッセージペイロード)

パラメータの定義

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

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

リクエストボディの定義

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

image.png

  • リクエストボディ(メッセージペイロード)は、 Operation Object の requestBodyRequest Body Object を用いて定義する
  • content が必須となっている
  • content の書き方は、レスポンスのコンテンツと同じ

コンポーネントの定義

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

  • コンポーネントはルートの components で、 Components Object を使って定義する
  • Components Object には schemasresponses, parameters など各所で再利用するためのコンポーネントを定義するプロパティが用意されている
  • 定義したコンポーネントを参照する際は、 $ref を使用する
    • 値には参照先のコンポーネントの URI を指定する
    • 同じファイル内のコンポーネントを参照する場合は、 # 始まりでコンポーネントのパスを記述する
    • 別のファイルのコンポーネントを参照する場合は ./filepath.yml#/components/... のように書くこともできる

例の定義

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

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

複数の例を定義する

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

  • examples を使うと複数の例を定義できる
  • examples は Map 型で、キーにパターンを識別する名前、値に Example Object を指定する
  • examples は Parameter Object や Media Type Object で使用できる
  • exampleexamples は排他の関係で、片方のみ定義できる(両方同時に定義することはできない)

APIサーバーを定義してAPIを実際に試す

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

  • servers で、APIを実際に試すときのアクセス先を定義できる
  • serversServer Object の配列で指定する
  • url で、APIにアクセスするときのベースとなる URL を指定する
    • この URL に paths で定義された個々のパスが接続された形でリクエストが実行される
  • APIの実行は個々のオペレーションにある [Try it out] ボタンから試せる

JSON Schema

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

数値

整数

schema:
  type: integer
OK
1
1.0
-1
0
NG
1.1
0.1
  • integer を指定すると、整数値であることを定義できる
  • 1.0 のように少数部が 0 の場合は整数として扱われる

実数

schema:
  type: number
OK
1.1
-1.2
10
1.2e3
NG
"20"
  • number を指定すると、少数を含んだ数値であることを定義できる
  • 指数表記も可能

倍数

schema:
  type: number
  multipleOf: 5
OK
0
5
15
-10
NG
6
12
  • multipleOf を使用すると、指定した値の倍数であることを定義できる

範囲(閉区間)

schema:
  type: integer
  minimum: 2
  maximum: 5
OK
2
3
5
NG
1
6
  • minimum で指定した値以上、 maximum で指定した値以下を定義できる

範囲(開区間)

schema:
  type: integer
  exclusiveMinimum: 2
  exclusiveMaximum: 5
OK
3
4
NG
2
5
  • exclusiveMinimum で指定した値より大きい、 exclusiveMaximum で指定した値より小さいことを定義できる

真偽値

schema:
  type: boolean
OK
true
false
NG
"true"
0
null
[]
  • boolean を指定すると、真偽値であることを定義できる
  • 0null のような、 JavaScript だと false 扱いになるような値は設定できない

null値

schema:
  type: "null"
  • リテラルの null ではなく、文字列で "null" と指定している点に注意
OK
null
NG
"null"
""
false
[]
0
  • "null" を指定すると、 null 値であることを定義できる

文字列

schema:
  type: string
OK
"foo"
"bar"
""
NG
0
false
["array"]
{"type": "object"}
null
  • typestring を指定すると、文字列であることを定義できる

文字数を定義する

schema:
  type: string
  minLength: 2
  maxLength: 4
OK
"ab"
"abcd"
"あいうえ"
"𠮷abc"
NG
"a"
"abcde"
"あいうえお"
  • minLength, maxLength を使用すると、文字数の最小と最大を定義できる
  • 文字数はUnicodeのコードポイントの数でカウントするので、漢字やサロゲートペア文字も1文字1つとしてカウントされる

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/

正規表現

schema:
  type: string
  pattern: ^\d{3}-[a-z]+$
  • 数値が3桁、ハイフンを挟んでアルファベット小文字が1つ以上続くという正規表現
OK
"012-foobar"
"987-test"
NG
"12-foo"
"012-123"

定数

schema:
  type: string
  const: "foo"
OK
"foo"
NG
"bar"
10
  • const を使用すると、特定の値のみを許可するように定義できる

デフォルト値

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

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

列挙型

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

# ブロックスタイルで定義した場合の例
schema:
  type: integer
  enum:
    - "aaa"
    - "bbb"
    - "ccc"
OK
"aaa"
"bbb"
"ccc"
null
NG
"AAA"
  • enum を使うと、その項目に設定可能な値を列挙できる
  • null は OK になる

列挙型を Component で定義して参照する

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"
  • componentsschemas であらかじめ列挙型を定義しておいて、各項目で参照するという使い方ができる

オブジェクト

schema:
  type: object
OK
{"foo": "FOO"}
{"fizz": "FIZZ", "buzz": "BUZZ"}
NG
1
true
null
"abc"
[1, 2, 3]
  • object を指定すると、オブジェクトであることを定義できる
  • これだけだと、オブジェクトの中身は何でもいいことになる

プロパティを定義する

schema:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
OK
{
    "id": 10,
    "name": "hoge"
}
{
    "id": 20,
    "name": "fuga",
    "age": 21
}
{
    "id": 30
}
NG
{
    "id": "10"
}
  • properties を使用すると、オブジェクトのプロパティのスキーマを定義できる
  • properties は Map で指定する
    • キーはプロパティの名前
    • 値は、そのプロパティのスキーマ定義(Schema Object)
  • properties だけの場合、プロパティが不足していたり余計なプロパティがあっても invalid にはならない

必須プロパティを定義する

schema:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
  required: ["id"]
OK
{
    "id": 10,
    "name": "foo"
}
{
    "id": 20
}
{
    "id": 30,
    "age": 18
}
NG
{
    "name": "bar"
}
  • required で必須のプロパティを定義できる
  • 配列で指定し、要素には必須とするプロパティの名前を設定する

未定義のプロパティを制限する

schema:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
  additionalProperties: false
OK
{
    "id": 10,
    "name": "foo"
}
{
    "id": 20
}
NG
{
    "id": 30,
    "name": "bar",
    "age": 20
}
  • additionalPropertiesfalse を設定すると、定義外のプロパティを設定できないように制限できる

未定義のプロパティのスキーマを定義する

schema:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
  additionalProperties:
    type: boolean
OK
{
    "id": 10,
    "name": "foo"
}
{
    "id": 20,
    "other": false
}
NG
{
    "id": 30,
    "other": "test"
}
  • additionalProperties に Schema Object を指定すると、未定義のプロパティのスキーマを定義できる

プロパティ名を正規表現で定義する

schema:
  type: object
  patternProperties:
    ^S_:
      type: string
    ^I_:
      type: integer
OK
{
    "S_1": "foo",
    "I_1": 10
}
{
    "S_21": "bar",
    "I_19": 6
}
{
    "another": "any"
}
NG
{
    "S_20": 21
}
{
    "I_17": false
}
  • patternProperties を使用すると、プロパティの名前を正規表現で定義できる
  • 正規表現がマッチしたプロパティのみが、指定したスキーマ定義と一致しなければならない

プロパティ名のスキーマを定義する

schema:
  type: object
  propertyNames:
    pattern: ^[A-Z][a-zA-Z]+$
    minLength: 2
    maxLength: 5
  • プロパティ名はアルファベットのみで先頭は大文字、さらに2文字以上5文字以下であるというスキーマ定義
OK
{
    "Id": 1,
    "Name": "foo"
}
NG
{
    "Weight": 65.4
}
{
    "id": 2,
    "name": "bar"
}
{
    "I": 3
}
  • propertNames を使用すると、プロパティ名のスキーマを定義できる
  • プロパティ名は文字列である必要があるので、少なくとも type: string は設定されている前提となる

プロパティの数を制限する

schema:
  type: object
  minProperties: 2
  maxProperties: 5
OK
{
    "id": 1,
    "name": "foo"
}
{
    "id": 2,
    "name": "bar",
    "age": 14,
    "tall": 165.2,
    "weight": 62.3
}
NG
{
    "id": 3
}
{
    "id": 2,
    "name": "fizz",
    "age": 14,
    "tall": 165.2,
    "weight": 62.3,
    "country": "jp"
}
  • minProperties でプロパティ数の最小数を、 maxProperties で最大数を定義できる

条件付き必須

schema:
  type: object
  dependentRequired:
    hoge: ["fuga"]
OK
{
    "id": 1
}
{
    "fuga": 10
}
{
    "hoge": "HOGE",
    "fuga": "FUGA"
}
NG
{
    "hoge": "HOGE"
}
  • dependentRequired を使用すると、あるプロパティが存在するときだけ別のプロパティを必須にする、という制御ができる
  • dependentRequired は Map で指定する
    • キーに指定したプロパティが存在した場合に、値に配列で指定した名前のプロパティたちが必須になる
    • foo: ["fizz", "buzz"] と指定すれば、 foo というプロパティが存在したときに fizz, buzz というプロパティが必須になる

If-Then-Else

schema:
  if:
    type: string
  then:
    pattern: ^[a-z]+$
  else:
    type: integer
OK
"hoge"
10
NG
"012"
true
  • if, then, else を用いると、条件が満たされたときだけ有効になるスキーマを定義できる
  • 検証対象の値が if で定義したスキーマ定義を満たす場合は、 then で指定したスキーマ定義も満たすかどうかが検証される
  • 検証対象の値が if で定義したスキーマを満たさない場合は、 else で指定したスキーマ定義を満たすかどうかが検証される
  • elsethen は、不要であればいずれかを省略することも可能
  • 「オブジェクトのプロパティが特定の値だった場合は」みたいなときは、以下のように const を利用する
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 という整数値のプロパティを必須で定義している
OK
{
    "id": 10,
    "name": "hoge"
}
{
    "id": 20,
    "name": "foo",
    "age": 12
}
{
    "id": 50,
    "name": "bar",
    "age": true
}
NG
{
    "id": 30,
    "name": "foo"
}
{
    "id": 40,
    "name": "foo",
    "age": "20"
}

配列

schema:
  type: array
OK
[1, 2, 3, 4, 5]
["a", "b", "c", "d"]
[1, "a", {"foo": "Foo"}]
NG
{"Not": "an array"}
  • typearray を指定すると、配列を表す
  • これだけの場合、配列の中身は何でもいい(数値でも文字列でもオブジェクトでも、なんでもアリ)

要素の型を限定する

schema:
  type: array
  items:
    type: number
OK
[1, 2, 3, 4, 5]
[]
NG
[1, 2, "3", 4, 5]
  • items で要素の型を限定できる
  • 指定された型以外の要素があるとNG
  • 空配列はOK

先頭から任意の数の要素の型を限定する

schema:
  type: array
  prefixItems:
    - type: number
    - type: string
    - enum: ["foo", "bar"]
    - enum: ["fizz", "buzz"]
OK
[1000, "Hoge", "foo", "fizz"]
[1200, "Fuga", "bar"]
[1300, "Foo", "foo", "bar", "extra"]
NG
[1000, "Hoge", "fizz", "foo"]
["str", "Hoge", "foo", "fizz"]
  • prefixItems を使用すると、先頭から任意の数の要素の型を限定できる
  • ここでは先頭4つの要素の型を、それぞれ以下のように定義している
    • 1つ目は数値のみ
    • 2つ目は文字列のみ
    • 3つ目は enum で "foo" または "bar" のみ
    • 4つ目は enum で "fizz" または "buzz" のみ
  • 要素が不足する場合はOK
  • prefixItems で定義した数より後ろの要素は何が来てもOK

prefixItems で定義した要素以外の要素は入れられないようにする

schema:
  type: array
  prefixItems:
    - type: number
    - type: string
  items: false
OK
[100, "text"]
[100]
NG
[100, "text", "foo"]
  • prefixItems の指定に加えて itemsfalse を設定すると、 prefixItems で定義した要素より後ろには要素を入れられなくなる

prefixItems で定義した要素以外の要素を限定する

schema:
  type: array
  prefixItems:
    - type: number
    - type: string
  items:
    type: number
OK
[100, "text"]
[100, "text", 1]
NG
[100, "text", "foo"]
  • prefixItems に加えて items で要素の型を定義すると、 prefixItems より後ろの要素は items で指定された型の値しか入れられなくなる

最低1つ含まれる要素を定義する

schema:
  type: array
  contains:
    type: number
OK
[1, 2, 3]
[1, "foo", false]
NG
["foo", "bar"]
[true, false]
[]
  • contains を使用すると、指定された条件を満たす要素が最低1つは存在していることを検証できる
  • 上記例では、型が number である要素が最低1つ入っていることを定義している

最低/最大 n 個含まれる要素を定義する

schema:
  type: array
  contains:
    type: number
  minContains: 2
  maxContains: 4
OK
[1, "foo", 2, "bar"]
[1, false, 2, 3, 4, "fizz"]
NG
[1, "foo", "bar"]
[1, 2, false, 3, 4, true, 5]
  • contains と合わせて minContains, maxContains を使用することで、要素数の最小数を最大数を定義できる

要素数を定義する

schema:
  type: array
  minItems: 1
  maxItems: 3
OK
[1]
["hoge", "fuga", "piyo"]
NG
[]
[1, 2, "foo", "bar"]
  • minItems, maxItems で、要素数の最小と最大を定義できる

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

schema:
  type: array
  uniqueItems: true
OK
[1, 2, 3, 4]
[
  {
    "age": 12,
    "name": "Hoge"
  },
  {
    "age": 15,
    "name": "Fuga"
  },
  {
    "age": 13,
    "name": "Piyo"
  }
]
[]
NG
[1, 2, 3, 1]
[
  {
    "age": 12,
    "name": "Hoge"
  },
  {
    "age": 15,
    "name": "Fuga"
  },
  {
    "name": "Hoge",
    "age": 12
  }
]
["foo", "bar", "fizz", "foo"]
  • uniqueItemstrue を設定すると、全ての要素がユニーク(重複が許されない)ことを定義できる

スキーマ定義を組み合わせる

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

  • Schema Object では、 oneOf, anyOf, allOf を使って複数のスキーマ定義を混ぜることができる
    • これ自体は JSON Schema で定義された仕様
  • それぞれの値は、 Schema Object の配列で指定する
  • oneOf は、スキーマ定義のうち1つだけにマッチしなければならない
    • 2つ以上にマッチしてはいけない
  • anyOf は、スキーマ定義のうちいずれか1つ以上にマッチすればいい
    • 複数にマッチしてもいい
    • どれともマッチしないのはダメ
  • allOf は、全てのスキーマ定義にマッチしなければならない

継承を表現する

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

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

ポリモーフィズムを表現する

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

  • ポリモーフィズムを表現するためには、 Schema Object の discriminator を使用する
    • これは JSON Schema にはない、 OpenAPI 独自拡張のプロパティ
  • discriminator には 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 などで複数のスキーマ定義を組み合わせた場合、 additionalPropertiesadditionalItems などの「未定義のプロパティ」の扱われ方について注意が必要になる。

schema:
  type: object
  allOf:
    - type: object
      properties:
        id: integer
  properties:
    name: string
  additionalProperties: false
  • allOf を用いて idname の2つのプロパティを組み合わせたスキーマを定義している
  • さらに additionalPropertiesfalse を設定することで、追加のプロパティを拒否している
  • この定義に以下のような JSON を入力すると、 invalid と判定される
NG
{
    "id": 1,
    "name": "foo"
}
  • これは additionalProperties が、それが宣言されているスキーマ定義の範囲内しか適用されないことが原因となっている(allOf などで組み合わせたスキーマ定義までは考慮されない)
  • この問題を回避するためには、 unevaluatedProperties を使用する
schema:
  type: object
  allOf:
    - type: object
      properties:
        id: integer
  properties:
    name: string
  unevaluatedProperties: false
OK
{
    "id": 1,
    "name": "foo"
}
NG
{
    "id": 2,
    "other": "hoge"
}
  • unevaluatedProperties は、「未評価のプロパティ」に対するスキーマを定義するための設定となっている
  • この「未評価」とは、 allOf などで組み合わせたスキーマ定義を全て評価した結果、残ったまだ評価されていないプロパティを指している
  • 使い方は additionalProperties と同じで、 false を指定すれば未評価のプロパティを拒否できるし、Schema Objectで定義を記述すれば未評価のプロパティの定義を制限することもできる
  • 配列の場合も同様で、 unevaluatedItems という設定項目が用意されている

仕様詳細

OpenAPI の仕様(書き方)について、もうちょっと詳しく見ていく。

全般

description

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

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

説明にコロンを含める

openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      description: "説明: post hoge"

image.png

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

複数行で記述する

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

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

CommonMark

  • description では CommonMark というマークアップ言語を使用できる
    • CommonMark は Markdown の仕様を標準化したもの(しようとしたもの)
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

  • だいたい Markdown と同じ感じで書けるっぽい
  • CommonMark の詳細な仕様は CommonMark Spec を参照のこと

リクエスト

パスごとにオペレーションで共通のパラメータを定義する

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

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

マルチパートの一部のデータのContent-Typeを指定する

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
送信されたリクエスト(一部)
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 を使用すると、マルチパートの特定のデータに対して Content-Type を付与することができる
  • encoding は Map で指定する
    • キーにエンコーディングの情報を設定したいマルチパートのデータの名前を指定
    • 値に Encoding Object を指定

レスポンス

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

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

image.png

  • Operation Objectresponses ではステータスコードごとにレスポンスを定義できるが、特定のステータスコード以外のデフォルトのレスポンスを定義したい場合は default というキーを使うことで定義できる

レスポンスのステータスコードにワイルドカードを使用する

openapi: '3.1.1'
info:
  title: API Title
  version: '1.0'
paths:
  /hoge:
    post:
      responses:
        "5XX":
          description: その他サーバーエラー
        "503":
          description: サーバーがダウンしてる

image.png

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

コールバックを定義する

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

  • Operation Objectcallbacks でコールバックを定義できる
  • コールバックは Map で、キーにコールバック先のURL、値に Path Item Object を指定する

コールバックとは

  • まず、コールバックが何なのかについて
  • コールバックとは、あるオペレーションを実行したときに API サーバーが自動的に呼び出す別の API リクエストのことを指す
  • 最初の例の yaml だと、 localhost が「APIサーバー」で、 some.host が「他のAPIサーバー」に対応する

コールバックのURLをリクエストの情報から動的に構築する

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

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

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

ランタイム式

  • コールバックで使用したランタイム式について、もう少し詳細な使い方を説明する
  • 公式解説書の説明は ここ
  • ランタイム式は文字列中に波括弧 ({}) で囲って記載する
  • ランタイム式の中では、まず以下の元となるオペレーションに含まれるいずれかの要素を参照する
説明
$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があったとする。

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 リクエストを投げたとする
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"
  ]
}
  • そして、レスポンスが次のような内容だったとする
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

タグ

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

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

タグの順序を指定する

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

  • OpenAPI Objecttags で、タグの順序を定義できる
  • tagsTag Object を配列で指定する
  • Tag Object は name が必須で、オペレーションの tags で指定したものと同じ名前になるように値を設定する
    • description で説明をつけることも可能
  • この配列で指定した順序が、そのままドキュメント上でのタグの順序として反映される
  • 実際にオペレーションで使用しているタグを全て定義する必要はない
    • 未定義のタグは、順不同(ツール依存)となる

webhooks

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

  • OpenAPI Objectwebhooks で Webhook の仕様を定義できる
  • webhooks は Map で指定し、キーに Webhook を一意に識別する名前、値に Path Item Object を指定する

認証方法の定義

基本(APIキー認証)

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

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

image.png

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

image.png

image.png

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

image.png

  • [Authorize] ボタンの鍵マークがロックされた状態になっているので、この状態で [Try it] で API を実行してみる
サーバーに届いたリクエストの情報
## Method
POST
## Request URI
requestURI = /echo/hoge
queryString = 

## Headers
host: localhost:8080
connection: keep-alive
...
x-api-key: TestApiKey
...
  • x-api-key というヘッダーに先ほどダイアログで入力した値が設定された状態でリクエストが届いている

説明

components:
  securitySchemes:
    apiKeyExample:
      type: apiKey
      name: X-Api-Key
      in: header
security:
  - apiKeyExample: []
  • API の認証方法を定義するには、次の2つの要素を定義する必要がある
    • components.securitySchemas
    • security
  • まず components.securitySchemas で認証方法の具体的な仕様を定義する
  • 次に security で、 securitySchemas で定義した認証方法のうち、実際に使用する認証方法を宣言する
securitySchemes
components:
  securitySchemes:
    apiKeyExample:
      type: apiKey
      name: X-Api-Key
      in: header
  • secruitySchemas は Map で、キーに認証方法を一意に識別できる名前を、値に Security Scheme Object を指定する
  • Security Schema Object では、まず認証の種類を type で指定する
  • type には以下のいずれかを指定できる
認証方法
apiKey APIキーを用いた認証
http HTTP認証
mutualTLS 相互TLS認証
oauth2 OAuth 2.0
openIdConnect OpenID Connect
  • apiKey は、例でも示したように独自ヘッダーなどを用いて認証用のキー情報を連携する認証方法になる
  • apiKey を指定した場合、 inname を追加で指定する必要がある
    • in には、APIキーをどのパラメータから渡すかを設定する
      • query, header, cookie のいずれかが指定できる
    • name には、 API キーを設定するパラメータの名前を設定する
      • ここでは inheader にしているので、ヘッダーの名前を指定していることになる
security
security:
  - apiKeyExample: []
  • security には、実際にAPIリクエスト時に使用する認証方法を指定する
  • security は配列で指定し、配列の各要素は Security Requirement Object で指定する
  • Security Requirement Object は、キーに securitySchemas で定義した認証方法の識別名を、値には認証で使用する追加のパラメータを string の配列で渡す
    • ただし、追加のパラメータの要否は認証の種類によって異なっている
    • apiKey の場合、追加のパラメータは必要ないので空の配列を渡しておけばいい
    • 認証の種類が oauth2openIdConnect の場合、ここにスコープの配列を渡すことになる

HTTP認証(Basic認証)

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

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

## Headers
...
authorization: Basic dGFybzpwYXNzd29yZA==
...
  • 上記はHTTP認証の、特にBasic認証の場合の例になる
  • HTTP認証の場合は typehttp を指定する
  • HTTP認証の場合、 schema が必須指定となる
    • schema には使用するHTTP認証スキームの名前を指定する
    • この名前は IANA Authentication Scheme registry に登録されている名前のいずれかを使用する
    • ここではBasic認証にするので basic と指定している
  • Swagger UI から入力できる認証情報がユーザー名とパスワードになっており、Try itでリクエストを送ると、 authorization ヘッダーにBasic認証の情報が載っていることがわかる

OpenID Connect

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

image.png

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

Keycloakの導入

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

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

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

image.png

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

image.png

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

image.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

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

image.png


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

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

image.png

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

image.png

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

image.png

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

image.png

Swagger-UI の導入

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 を作成する
  • 内容は以下
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 は通るようにしないといけないので、そこはとりあえず全通しの設定を入れておく
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 以下任意のリクエストを受け付けてリクエストの情報をコンソールに書き出すコントローラを用意
雑な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

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

image.png

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

image.png

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

image.png

  • Swagger-UI の画面から [Try it] でAPIを叩いてみる
サーバーに届いたリクエストの情報
###############################################
## 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 のトークンが渡されていることがわかる
説明
components:
  securitySchemes:
    oidcExample:
      type: openIdConnect
      openIdConnectUrl: http://localhost:8080/realms/test-realm/.well-known/openid-configuration
security:
  - oidcExample: []
  • OpenID Connect の場合、 typeopenIdConnect とする
  • OpenID Connect の場合、 openIdConnectUrl が必須となるのでディスカバリーエンドポイントの URL を指定する
  • security の配列には実行に必要な scope を指定する
    • 今回は、認可は特に試してないので空にしている

Swagger UI

Swagger UI は OpenAPI のAPI定義ファイルから HTML のドキュメントを生成するためのツール。

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

ドキュメント生成

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

Swagger UI をダウンロードする

dist フォルダを抜き出す

image.png

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

Tomcat に配備する

image.png

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

image.png

ドキュメント化したいAPI定義ファイルを配置する

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

swagger-initializer.js を修正する

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

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

Docker で生成する

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

$ 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

コード生成(OpenAPI Generator)

Swagger Codegen と OpenAPI Generator

  • OpenAPI の API 定義ファイルからクライアントやサーバーのソースコードを自動生成するツールがいくつか存在する
  • 有名なものとして Swagger CodegenOpenAPI Generator がある
  • もともとは Swagger Codegen として開発されていたが、OpenAPI 3.0 になかなか対応しないとか管理している会社による強引な修正がされるようになって有志がフォークしてできたのが OpenAPI Generator らしい
  • 一応、現在は Swagger Codegen も OpenAPI 3.0 以上に対応しているようだが、開発がよりアクティブなのは OpenAPI Generator のほうなので、ここでは OpenAPI Generator の使い方について調べることにする

インストール

  • OpenAPI Generator の実体は Java アプリで、 jar ファイルを落としてきてコマンドラインから利用する
  • jar ファイルは Maven のセントラルリポジトリ から openapi-generator-cli-x.x.x.jar をダウンロードして入手する
  • 実行には Java 11 以上が必要
動作確認
$ 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 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 コマンド

コマンド例
$ 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 の一覧は こちらのページ で確認できる
      • もしくは list コマンドでも確認できる
  • -i で、OpenAPI の API 定義ファイルを指定する
  • -o は出力先のディレクトリを指している
  • 以下のようなファイルたちが出力される
出力結果
│  .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
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 コマンド

$ 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 コマンド

$ 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 でも確認できる

サーバーサイド

どの generator を使う?

  • Java でサーバーサイドの RESTful Web API の実装といえば JAX-RS か Spring Web MVC が個人的には多い気がするので、これらの実装を生成する方法について調べる
  • Spring だと、該当する generator は spring だけみたいなのでいいが、 JAX-RS は何かいっぱいある
    • jaxrs-cxf
    • jaxrs-cxf-cdi
    • jaxrs-cxf-extended
    • jaxrs-jersey
    • jaxrs-resteasy
    • jaxrs-resteasy-eap
    • jaxrs-spec
  • cxf は Apache CXF という Apache プロジェクトにおける JAX-RS 実装らしい
  • cxf, jersey, resteasy と付いている generator は、それぞれの実装に依存した形でソースが生成されるのだろう(たぶん)
  • 唯一、具体的な実装名が含まれていない jaxrs-spec は、特定の実装ライブラリに依存しない JAX-RS で定義されたAPIのみを利用した形でソースが生成される
  • ということで、基本は jaxrs-spec を選択すればいい気がする

jaxrs-spec ジェネレータ

生成されるファイル

以下のAPI定義を元にソースを生成してみる。

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

生成されたのは以下。

生成されたファイル
└─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 クラス
  • FooApipaths の定義を元に生成されたリソースクラスのファイルで、以下のようになっている(見やすくするため、一部改行を調整している)
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 は以下のような感じ
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)を用意する
config.yml
interfaceOnly: true
  • この YAML ファイルを指定して、 generate コマンドを実行する
  • 設定ファイルは -c オプションで指定する
java -jar openapi-generator-cli-7.13.0.jar generate \
  -i sample.yml \
  -g jaxrs-spec \
  -c config.yml \
  -o output
  • すると、リソースクラスは以下のようにインタフェースで出力される
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 を設定する
config.yml
useJakartaEe: true
  • 生成されたリソースクラスは以下のようになる
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 のオプション全量は こちら
  • この中から、個人的に使うことになりそうだなぁと思ったものをピックアップする
オプション デフォルト値 説明
apiPackage org.openapitools.api リソースクラスの出力先パッケージ
dateLibrary legacy 日付クラスに何を使うか。
joda: Joda
legacy: java.util.Date
java8-localtime: LocalDateTime
java8: JSR310のクラス
generateBuilders false モデルクラスのビルダーを生成するかどうか
modelPackage org.openapitools.model モデルクラスの出力先パッケージ
useBeanValidation true Bean Validationを使用するかどうか

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

  • 以下のようなAPI定義を元にソースを生成してみる
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
  • なお、継承関係のイメージは以下のような感じ
  • これを生成すると、リソースクラスの引数が HogePostRequest という型で生成されるが、その内容が以下のようになっている
HogePostRequest.java
public class HogePostRequest   {
  private String childType;
  private String bar;
  • bar フィールドしか持っていなくて、 foo を受け取れない形になっている
  • このままだと、 Foo 型のオブジェクトを受け取れないことになる
  • なんかポリモーフィズムが動かないという Issue は Open のまま存在しているが、ここで記載している内容とはちょっと違うっぽい
  • ちなみに、 spring ジェネレータの場合はいい感じに出力してくれるので jaxrs-spec 固有のバグなんじゃないかなぁと思っている

spring ジェネレータ

  • jaxrs-spec ジェネレータの検証で使用していたものと同じ yaml を使って spring ジェネレータでソースを生成してみる
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
  • 生成されたファイルは以下のようになっている
生成されたファイル
│  .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 のコントローラが生成されている
  • 以下が、実際に出力されたソース(見やすくするため改行はいじっている)
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);
    }
}
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 のオプション全量は こちら
  • この中から、個人的に使うことになりそうだなぁと思ったものをピックアップする
オプション デフォルト値 説明
apiPackage org.openapitools.api APIクラスの出力先パッケージ
basePackage org.openapitools ベースのパッケージ
configPackage org.openapitools.configuration 設定クラスの出力先パッケージ
dateLibrary legacy 日付クラスに何を使うか。
joda: Joda
legacy: java.util.Date
java8-localtime: LocalDateTime
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 というものが用意されている
  • 以下のようなプロジェクトを用意して生成させてみる
フォルダ構成
|-pom.xml
`-src/main/
  `-resources/
    `-api.yml
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>
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
  • 以下のコマンドでソースが自動生成される
mvn clean compile
  • 以下のような感じで生成される
一部のみ記載
|-pom.xml
|-src/
| :
`-target/
  |-generated-sources/
  : |-openapi/
    : |-pom.xml
      |-README.md
      `-src/
        |-main/
        | :
        `-gen/
          `-java/
            `-org/openapitools/
              |-api/
              `-model/

説明

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-plugingenerate ゴールを使用する
    • デフォルトで generate-resources フェーズに紐づくようになっている
  • configuration で各種設定をしていく
    • inputSpec で、入力となる API 定義ファイルを指定する
    • generatorName で、使用するジェネレータを指定する
    • configOptions でジェネレータの設定を指定できる
      • sourceFolder はジェネレータが出力するソースコードのディレクトリパスを指定するためのオプション
      • jaxrs-spec ジェネレータのデフォルト値は src/gen/java
      • このオプションをデフォルト値でいいので設定しておくと、自動的に出力先のディレクトリがソースディレクトリとして認識されるようになる
        • (デフォルト値があるのに未指定だと認識されないのがなんか気持ち悪い)
    • そのほかの設定については 公式ドキュメント を参照
出力ファイル
|-pom.xml
|-src/
| :
`-target/
  |-generated-sources/
  : |-openapi/
    : |-pom.xml
      |-README.md
      `-src/
        |-main/
        | :
        `-gen/
          `-java/
            `-org/openapitools/
              |-api/
              `-model/
  • generate ゴールの結果は target/generated-sources の下に出力される
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定義の内容次第で必要な依存関係は増減すると思うので、実際のプロジェクトごとに要調整

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

フォルダ構成
|-pom.xml
`-src/main/resources/
  |-api.yml
  `-openapi-generator-conf.yml
openapi-generator-conf.yml
interfaceOnly: true
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

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
コマンド
java -jar openapi-generator-cli-7.13.0.jar generate \
    -i sample.yml \
    -o output \
    -g java
  • Java のクライアントサイドのソースを生成する場合は、 java ジェネレータを使用する
  • 以下が、実際に出力されたファイルたち
出力結果
│  .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
  • なにやら色々出力されている
  • 出力されたプロジェクトのルートに移動し、下記コマンドを実行してローカルリポジトリにインストールする
ローカルリポジトリにインストール
mvn install
  • 続いて、全く別の Maven プロジェクトを作成する
新規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 でローカルリポジトリにインストールしたクライアントモジュールを指している
pom.xml
    <dependency>
      <groupId>org.openapitools</groupId>
      <artifactId>openapi-java-client</artifactId>
      <version>1.0</version>
    </dependency>
  • src/main/java/sandbox/App.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 でモックサーバーを立ち上げる
    • モックサーバーについては こちら を参照
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 を実行してみる
実行結果
class FooIdGet200Response {
    id: 0
    name: string
}

説明

利用方法

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

ドキュメント

  • 出力されたファイルたちのルートに README.md があり、ここにプロジェクトのビルド方法やその後の使い方が詳細に書かれている
  • クライアントライブラリとしての利用方法も描かれており、個々の API の詳細へのリンクも記載されている
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 へリンクされており、そちらにさらに詳細が出力されている
docs/DefaultApi.md(一部)
...
### Parameters

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

### Return type

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

使い方

        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 のインスタンスは、 ConfigurationgetDefaultApiClient で取得できる
  • Configuration.getDefaultApiClient の実装を見に行くと、以下のようになっている
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回目以降は同じインスタンスを返すようになっている
  • 各クラスの関係を整理すると、以下のような感じ

利用する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
Gson 2.10.1
feign-hc5 OpenFeign 13.2.1/HttpClient5 5.4.2 Jackson 2.17.1 or
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
Jackson 2.17.1
resttemplate Spring RestTemplate 5.3.33
(useJakartaEetrue の場合は 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
Jackson 2.17.1
native Java native HttpClient Jackson 2.17.1
microprofile Microprofile client 2.0 JSON-B 1.0.2 or
Jackson 2.17.1
apache-httpclient Apache httpclient 5.2.1 Jackson 2.17.1
  • デフォルトは okhttp-gson となる

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

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

大まかな基本方針・前提

  • ファイル編集は VS Code を使用する
  • API ドキュメントは、 Swagger UI で表示できるようにする
  • コード生成は OpenAPI Generator の Maven プラグインで実行できるようにする
  • 基本は分割したままの管理だが、その他使用するツールが単一ファイルしかサポートしていない場合も想定して、結合する方法についても調べておく
    • 自分が知っているものとしては、例えば Azure の API Management で API を OpenAPI の API 定義ファイルからロードすることができるが、その際は単一のファイルでないと読み込ませられない(はず)

実装

  • 細かい説明はおいおいしていくとして、まずはざっとファイル分割した場合の構成と中身を記載する
ファイル構成
openapi/
|-api.yml
|-components/
| `-book.yml
`-paths/
  `-books.yml
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"
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"
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件取得の3種類のエンドポイントを定義している

説明

ファイル構成

ファイル構成
openapi/
|-api.yml
|-components/
| `-book.yml
`-paths/
  `-books.yml
  • ルートとなるファイルと、分割したファイルで構成している
    • 分割の方法については色々考えられるだろうが、ここではとりあえず pathscomponents など、 OpenAPI Object で定義されたプロパティ単位でフォルダを切ってみている
    • paths については「/books で始まる API」のようにリソースのまとまりでファイルを分けてみている
    • components については、ひとまずリソース単位で分ける感じにしてみている
    • あくまで例の1つなので、適宜カスタマイズすればいい

ルートファイル

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 を使って分割したファイル内の定義を参照する形にしている
  • ここで、いくつか特殊な記法を用いているのでそれぞれ解説する
スラッシュのエスケープ
  /books:
    $ref: "./paths/books.yml#/paths/~1books"
  • $ref の中に ~1 という見慣れない表記がある
  • これはスラッシュ (/ ) のエスケープで、 JSON Pointer の仕様に従っている
  • つまり、 ~1books/books を表している
  • ちなみにチルダ (~) がパスに含まれる場合は ~0 で記述できる
波括弧のエスケープ
  /books/{id}:
    $ref: "./paths/books.yml#/paths/~1books~1%7Bid%7D"
  • この %7Bid%7D の部分はパーセントエンコードになっていて、 {id} を意味している
  • これ自体は本来の OpenAPI の仕様的には必要ないが、 OpenAPI Generator のバグに対応するため仕方なくこう記載している
  • {id} のままだと OpenAPI Generator でソースを生成しようとしたときにエラーが発生する
  • これを回避するために、仕方なくパーセントエンコードをしている
  • 詳しくは以下の Issue を参照

各分割ファイル

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

API定義

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

フォルダ構成
tomcat/webapps/swagger/
 |-swagger-initializer.js
 |-openapi/
 : |-api.yml
   |-components/
   `-paths/
  • 先ほどのファイル類を Swagger UI をデプロイしているところと同じ場所に配置する
swagger-initialzier.js
...
  window.ui = SwaggerUIBundle({
    url: "openapi/api.yml",
    ...
  });

  //</editor-fold>
};
  • swagger-initializer.js の url に、ルートとなる API 定義ファイルへのパスを設定する

実行結果

localhost_8080_swagger__urls.primaryName=API.png

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

OpenAPI Generator (Maven プラグイン)

フォルダ構成
|-pom.xml
`-src/main/
  |-java/
  `-resources/
    `-openapi/
      |-api.yml
      |-components/
      `-paths/
  • src/main/resources/openapi の下に API 定義ファイルを配置している
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 定義ファイルのパスを指定する

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

エラーログ
[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

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

ファイルを連結させる

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

Swagger CLI と Redocly CLI

  • 分割された OpenAPI の API 定義ファイルを1つにまとめるツールとして、かつては Swagger CLI というのが存在した
  • ただ、現在は開発が止まっていて GitHub リポジトリもアーカイブされている
  • このリポジトリの冒頭でも説明されているが、現在は Redocly CLI への移行が推奨されている
  • ということで、この Redocly CLI で API 定義ファイルを連結させる
  • Redocly CLI は Node のアプリなので npm でインストールできる
redocly-cliのインストール
npm i -g @redocly/cli@latest
  • Docker イメージも用意されているので、そちらでも可
  • 今回は Docker でやってみる
  • api.yml があるディレクトリまで移動して、以下のコマンドを実行する
Dockerを使う場合
docker run --rm -v `pwd`:/spec redocly/cli bundle /spec/api.yml -o /spec/result.yml
  • bundle コマンドの引数にエントリとなるルートファイルを指定して、 -o で出力先ファイルを指定している
  • 以下が、実際に出力されたファイル
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 というライブラリ
  • OpenAPI Support というのがあって、 OpenAPI の API 定義ファイルをそのままの形で読み込んでバリデーションに利用できる(Schema 部分だけを抽出とかしなくていい)

実装

|-build.gradle
`-src/main/
  |-java/
  | `-sandbox/jsonschema/
  |   `-Main.java
  `-resources/
    `-schema/
      `-sample.yml
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"
}
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
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");
    }
}
実行結果
==========
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
==========

説明

build.gradle
    implementation "com.networknt:json-schema-validator:1.5.7"
Main.java
        JsonSchemaFactory factory = 
            JsonSchemaFactory.getInstance(
                SpecVersion.VersionFlag.V202012,
                builder -> builder.metaSchema(OpenApi31.getInstance())
                                  .defaultMetaSchemaIri(OpenApi31.getInstance().getIri())
            );
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:/// で始める
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());
            }
        }
  • JsonSchemavalidate メソッドで検証を実行できる
  • 結果は ValidationMessageSet で返される
  • 問題ない場合は空が返り、問題がある場合は違反内容ごとに ValidationMessage が設定されている
  • なお、試した限りでは YAML が分割されていても、問題なく $ref で指定した先のファイルまで見て検証してくれた

モックサーバー

  • API 定義ファイルをもとにモックサーバーを作る方法
  • モックサーバーには Prism というツールを使用する
  • ドキュメントは こちら

起動

  • npm でインストールできるが、 Docker イメージ も用意されているので Docker を使う
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 定義を読み込んで起動してみる。

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 で単純にアクセスしてみる
$ 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 を指定する

$ 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 が返却される

ステータスコードを指定する

$ 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"}]
  • Prefercode=<ステータスコード> を指定すると、レスポンスのステータスコードを変更できる
  • 同時に example を指定したい場合はカンマで区切る
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

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

デザインファーストのアプローチ

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

真の単一ソースを維持する

  • 重複があるとダブルメンテになるから1つになるようにしようって話っぽい
  • 「単一」というのは重複をなくすって意味で、ファイルを1つにしようって意味ではないと思う(私見)

OpenAPI 定義はソースと同じように管理する

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

OpenAPI 定義をユーザーにも提供する

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

OpenAPI定義を手書きする必要は無い

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

大規模なAPIを記述する

同じ記述を繰り返さない

  • 定義内に同じ記述が現れたら、コンポーネント定義で共通化する
  • コンポーネント定義は他のファイルのものでも参照できるので、異なるAPI定義間で共有することもできる

ファイルを分割する

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

タグを使って整理する

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

参考

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?