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
が一般的らしい。知らんけど)
- サンプルのファイルが作成されるので、エディタの右上にあるプレビューボタンを押す
- 生成されたドキュメントが確認できる
ざっくり一巡り
細かい書き方・使い方について入る前に、ざっくりと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
- パスの下には、そのパスで使用できるオペレーションの定義(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
- 各 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.
- Response Object は
コンテンツ定義
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"
- 各 Response Object の
content
で、レスポンスのコンテンツを定義できる - コンテンツの定義は Media Type Object でコンテンツタイプごとに定義する
- 各コンテンツでは、
schema
で具体的なレスポンスコンテンツの構造を定義できる- この
schema
の記述は、JSON Schema Specification Draft 2020-12 で定義されたものをベースとしている- 一部 OpenAPI 用に拡張したりしているっぽい
- この
レスポンスヘッダーの定義
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
-
Response Object の
headers
で、そのレスポンスで返すヘッダーを定義できる -
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
- パラメータは、各 Operation Object の
parameters
で Parameter Object の配列で定義する -
name
には、パラメータの名前を指定する -
in
には、そのパラメータがどこから入力されるかを指定する-
path
,query
,header
,cookie
のいずれかを指定する -
path
はパスパラメータを指す- この場合、パス定義の中のテンプレート(波括弧(
{}
)で囲った部分)とname
が一致している必要がある
- この場合、パス定義の中のテンプレート(波括弧(
-
query
はクエリパラメータを指す
-
-
required
は、そのパラメータが必須かどうかを boolean で指定する-
true
を設定した場合、そのパラメータは必須パラメータとなる - 基本的に指定は任意
- ただし、
in
にpath
を指定している場合はrequired: true
を必ず設定しなければならない
-
-
schema
は必ず指定しなければならない - Parameter Object では、
name
とin
の指定は必須となっている
リクエストボディの定義
openapi: '3.1.1'
info:
title: API Title
version: '1.0'
paths:
/hoge/{foo}:
get:
# リクエストボディの定義
requestBody:
content:
# コンテンツタイプごとにボディの定義を記述できる
application/json:
schema:
type: integer
- リクエストボディ(メッセージペイロード)は、 Operation Object の
requestBody
で Request 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
- コンポーネントはルートの
components
で、 Components Object を使って定義する - Components Object には
schemas
やresponses
,parameters
など各所で再利用するためのコンポーネントを定義するプロパティが用意されている - 定義したコンポーネントを参照する際は、
$ref
を使用する- 値には参照先のコンポーネントの URI を指定する
- 同じファイル内のコンポーネントを参照する場合は、
#
始まりでコンポーネントのパスを記述する- これは RFC6901 の JSON Pointer で定義された仕様に従っている
- 別のファイルのコンポーネントを参照する場合は
./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
-
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
-
examples
を使うと複数の例を定義できる -
examples
は Map 型で、キーにパターンを識別する名前、値に Example Object を指定する -
examples
は Parameter Object や Media Type Object で使用できる -
example
とexamples
は排他の関係で、片方のみ定義できる(両方同時に定義することはできない)
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
-
servers
で、APIを実際に試すときのアクセス先を定義できる -
servers
は Server Object の配列で指定する -
url
で、APIにアクセスするときのベースとなる URL を指定する- この URL に
paths
で定義された個々のパスが接続された形でリクエストが実行される
- この URL に
- APIの実行は個々のオペレーションにある [Try it out] ボタンから試せる
JSON Schema
- 各パラメータの型やリクエスト・レスポンスボディの定義は JSON Schema (Draft 2020-12)の仕様に則って記述する
- ここでは、 Schema Object = JSON Schema の書き方について確認する
- ただし、 OpenAPI で使用する定義は JSON Schema と完全に互換性があるわけではなく、一部独自の拡張を入れているものもあるので注意
数値
整数
schema:
type: integer
1
1.0
-1
0
1.1
0.1
-
integer
を指定すると、整数値であることを定義できる -
1.0
のように少数部が0
の場合は整数として扱われる
実数
schema:
type: number
1.1
-1.2
10
1.2e3
"20"
-
number
を指定すると、少数を含んだ数値であることを定義できる - 指数表記も可能
倍数
schema:
type: number
multipleOf: 5
0
5
15
-10
6
12
-
multipleOf
を使用すると、指定した値の倍数であることを定義できる
範囲(閉区間)
schema:
type: integer
minimum: 2
maximum: 5
2
3
5
1
6
-
minimum
で指定した値以上、maximum
で指定した値以下を定義できる
範囲(開区間)
schema:
type: integer
exclusiveMinimum: 2
exclusiveMaximum: 5
3
4
2
5
-
exclusiveMinimum
で指定した値より大きい、exclusiveMaximum
で指定した値より小さいことを定義できる
真偽値
schema:
type: boolean
true
false
"true"
0
null
[]
-
boolean
を指定すると、真偽値であることを定義できる -
0
やnull
のような、 JavaScript だとfalse
扱いになるような値は設定できない
null値
schema:
type: "null"
- リテラルの
null
ではなく、文字列で"null"
と指定している点に注意
null
"null"
""
false
[]
0
-
"null"
を指定すると、null
値であることを定義できる
文字列
schema:
type: string
"foo"
"bar"
""
0
false
["array"]
{"type": "object"}
null
-
type
にstring
を指定すると、文字列であることを定義できる
文字数を定義する
schema:
type: string
minLength: 2
maxLength: 4
"ab"
"abcd"
"あいうえ"
"𠮷abc"
"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つ以上続くという正規表現
"012-foobar"
"987-test"
"12-foo"
"012-123"
-
pattern
で文字列の内容を正規表現で制限できる - 正規表現の構文は ECMA-262 に準拠している
- 詳細な構文については以下を参照
定数
schema:
type: string
const: "foo"
"foo"
"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"
-
default
を使用すると、値が指定されなかった場合のデフォルト値を表現できる - ただし、これはドキュメントなどを生成したときに「デフォルト値があるよ」ということを表現するためのもので、スキーマ検証の際にデフォルト値があることを考慮して値が無くても補完するためのものではない
列挙型
# フロースタイルで定義した場合の例
schema:
type: string
enum: ["aaa", "bbb", "ccc"]
# ブロックスタイルで定義した場合の例
schema:
type: integer
enum:
- "aaa"
- "bbb"
- "ccc"
"aaa"
"bbb"
"ccc"
null
"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"
-
components
のschemas
であらかじめ列挙型を定義しておいて、各項目で参照するという使い方ができる
オブジェクト
schema:
type: object
{"foo": "FOO"}
{"fizz": "FIZZ", "buzz": "BUZZ"}
1
true
null
"abc"
[1, 2, 3]
-
object
を指定すると、オブジェクトであることを定義できる - これだけだと、オブジェクトの中身は何でもいいことになる
プロパティを定義する
schema:
type: object
properties:
id:
type: integer
name:
type: string
{
"id": 10,
"name": "hoge"
}
{
"id": 20,
"name": "fuga",
"age": 21
}
{
"id": 30
}
{
"id": "10"
}
-
properties
を使用すると、オブジェクトのプロパティのスキーマを定義できる -
properties
は Map で指定する- キーはプロパティの名前
- 値は、そのプロパティのスキーマ定義(Schema Object)
-
properties
だけの場合、プロパティが不足していたり余計なプロパティがあっても invalid にはならない
必須プロパティを定義する
schema:
type: object
properties:
id:
type: integer
name:
type: string
required: ["id"]
{
"id": 10,
"name": "foo"
}
{
"id": 20
}
{
"id": 30,
"age": 18
}
{
"name": "bar"
}
-
required
で必須のプロパティを定義できる - 配列で指定し、要素には必須とするプロパティの名前を設定する
未定義のプロパティを制限する
schema:
type: object
properties:
id:
type: integer
name:
type: string
additionalProperties: false
{
"id": 10,
"name": "foo"
}
{
"id": 20
}
{
"id": 30,
"name": "bar",
"age": 20
}
-
additionalProperties
にfalse
を設定すると、定義外のプロパティを設定できないように制限できる
未定義のプロパティのスキーマを定義する
schema:
type: object
properties:
id:
type: integer
name:
type: string
additionalProperties:
type: boolean
{
"id": 10,
"name": "foo"
}
{
"id": 20,
"other": false
}
{
"id": 30,
"other": "test"
}
-
additionalProperties
に Schema Object を指定すると、未定義のプロパティのスキーマを定義できる
プロパティ名を正規表現で定義する
schema:
type: object
patternProperties:
^S_:
type: string
^I_:
type: integer
{
"S_1": "foo",
"I_1": 10
}
{
"S_21": "bar",
"I_19": 6
}
{
"another": "any"
}
{
"S_20": 21
}
{
"I_17": false
}
-
patternProperties
を使用すると、プロパティの名前を正規表現で定義できる - 正規表現がマッチしたプロパティのみが、指定したスキーマ定義と一致しなければならない
プロパティ名のスキーマを定義する
schema:
type: object
propertyNames:
pattern: ^[A-Z][a-zA-Z]+$
minLength: 2
maxLength: 5
- プロパティ名はアルファベットのみで先頭は大文字、さらに2文字以上5文字以下であるというスキーマ定義
{
"Id": 1,
"Name": "foo"
}
{
"Weight": 65.4
}
{
"id": 2,
"name": "bar"
}
{
"I": 3
}
-
propertNames
を使用すると、プロパティ名のスキーマを定義できる - プロパティ名は文字列である必要があるので、少なくとも
type: string
は設定されている前提となる
プロパティの数を制限する
schema:
type: object
minProperties: 2
maxProperties: 5
{
"id": 1,
"name": "foo"
}
{
"id": 2,
"name": "bar",
"age": 14,
"tall": 165.2,
"weight": 62.3
}
{
"id": 3
}
{
"id": 2,
"name": "fizz",
"age": 14,
"tall": 165.2,
"weight": 62.3,
"country": "jp"
}
-
minProperties
でプロパティ数の最小数を、maxProperties
で最大数を定義できる
条件付き必須
schema:
type: object
dependentRequired:
hoge: ["fuga"]
{
"id": 1
}
{
"fuga": 10
}
{
"hoge": "HOGE",
"fuga": "FUGA"
}
{
"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
"hoge"
10
"012"
true
-
if
,then
,else
を用いると、条件が満たされたときだけ有効になるスキーマを定義できる - 検証対象の値が
if
で定義したスキーマ定義を満たす場合は、then
で指定したスキーマ定義も満たすかどうかが検証される - 検証対象の値が
if
で定義したスキーマを満たさない場合は、else
で指定したスキーマ定義を満たすかどうかが検証される -
else
とthen
は、不要であればいずれかを省略することも可能 - 「オブジェクトのプロパティが特定の値だった場合は」みたいなときは、以下のように
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
という整数値のプロパティを必須で定義している
{
"id": 10,
"name": "hoge"
}
{
"id": 20,
"name": "foo",
"age": 12
}
{
"id": 50,
"name": "bar",
"age": true
}
{
"id": 30,
"name": "foo"
}
{
"id": 40,
"name": "foo",
"age": "20"
}
配列
schema:
type: array
[1, 2, 3, 4, 5]
["a", "b", "c", "d"]
[1, "a", {"foo": "Foo"}]
{"Not": "an array"}
-
type
にarray
を指定すると、配列を表す - これだけの場合、配列の中身は何でもいい(数値でも文字列でもオブジェクトでも、なんでもアリ)
要素の型を限定する
schema:
type: array
items:
type: number
[1, 2, 3, 4, 5]
[]
[1, 2, "3", 4, 5]
-
items
で要素の型を限定できる - 指定された型以外の要素があるとNG
- 空配列はOK
先頭から任意の数の要素の型を限定する
schema:
type: array
prefixItems:
- type: number
- type: string
- enum: ["foo", "bar"]
- enum: ["fizz", "buzz"]
[1000, "Hoge", "foo", "fizz"]
[1200, "Fuga", "bar"]
[1300, "Foo", "foo", "bar", "extra"]
[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
[100, "text"]
[100]
[100, "text", "foo"]
-
prefixItems
の指定に加えてitems
にfalse
を設定すると、prefixItems
で定義した要素より後ろには要素を入れられなくなる
prefixItems で定義した要素以外の要素を限定する
schema:
type: array
prefixItems:
- type: number
- type: string
items:
type: number
[100, "text"]
[100, "text", 1]
[100, "text", "foo"]
-
prefixItems
に加えてitems
で要素の型を定義すると、prefixItems
より後ろの要素はitems
で指定された型の値しか入れられなくなる
最低1つ含まれる要素を定義する
schema:
type: array
contains:
type: number
[1, 2, 3]
[1, "foo", false]
["foo", "bar"]
[true, false]
[]
-
contains
を使用すると、指定された条件を満たす要素が最低1つは存在していることを検証できる - 上記例では、型が
number
である要素が最低1つ入っていることを定義している
最低/最大 n 個含まれる要素を定義する
schema:
type: array
contains:
type: number
minContains: 2
maxContains: 4
[1, "foo", 2, "bar"]
[1, false, 2, 3, 4, "fizz"]
[1, "foo", "bar"]
[1, 2, false, 3, 4, true, 5]
-
contains
と合わせてminContains
,maxContains
を使用することで、要素数の最小数を最大数を定義できる
要素数を定義する
schema:
type: array
minItems: 1
maxItems: 3
[1]
["hoge", "fuga", "piyo"]
[]
[1, 2, "foo", "bar"]
-
minItems
,maxItems
で、要素数の最小と最大を定義できる
要素に重複がないことを定義する
schema:
type: array
uniqueItems: true
[1, 2, 3, 4]
[
{
"age": 12,
"name": "Hoge"
},
{
"age": 15,
"name": "Fuga"
},
{
"age": 13,
"name": "Piyo"
}
]
[]
[1, 2, 3, 1]
[
{
"age": 12,
"name": "Hoge"
},
{
"age": 15,
"name": "Fuga"
},
{
"name": "Hoge",
"age": 12
}
]
["foo", "bar", "fizz", "foo"]
-
uniqueItems
にtrue
を設定すると、全ての要素がユニーク(重複が許されない)ことを定義できる
スキーマ定義を組み合わせる
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
-
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
-
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
- ポリモーフィズムを表現するためには、 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
などで複数のスキーマ定義を組み合わせた場合、 additionalProperties
や additionalItems
などの「未定義のプロパティ」の扱われ方について注意が必要になる。
schema:
type: object
allOf:
- type: object
properties:
id: integer
properties:
name: string
additionalProperties: false
-
allOf
を用いてid
とname
の2つのプロパティを組み合わせたスキーマを定義している - さらに
additionalProperties
にfalse
を設定することで、追加のプロパティを拒否している - この定義に以下のような JSON を入力すると、 invalid と判定される
{
"id": 1,
"name": "foo"
}
- これは
additionalProperties
が、それが宣言されているスキーマ定義の範囲内しか適用されないことが原因となっている(allOf
などで組み合わせたスキーマ定義までは考慮されない) - この問題を回避するためには、
unevaluatedProperties
を使用する
schema:
type: object
allOf:
- type: object
properties:
id: integer
properties:
name: string
unevaluatedProperties: false
{
"id": 1,
"name": "foo"
}
{
"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
- OpenAPI 定義は、様々な箇所に
description
というプロパティが用意されており、自然言語での説明を記載できるようになっている
説明にコロンを含める
openapi: '3.1.1'
info:
title: API Title
version: '1.0'
paths:
/hoge:
post:
description: "説明: post hoge"
- コロン (
:
) は 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
-
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

## Table
|id|name|
|---|---|
|1|foo|
|2|bar|
- だいたい 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??
- Path Item Object の
parameters
を定義すると、そのパス配下の全てのオペレーションで共通のパラメータを定義できる - パラメータは Operation Object の
parameters
で上書きできる- パラメータは
name
とin
の組み合わせで一意に特定される - 上書きはできるが、削除はできない
- パラメータは
マルチパートの一部のデータの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: デフォルトのレスポンス
-
Operation Object の
responses
ではステータスコードごとにレスポンスを定義できるが、特定のステータスコード以外のデフォルトのレスポンスを定義したい場合はdefault
というキーを使うことで定義できる
レスポンスのステータスコードにワイルドカードを使用する
openapi: '3.1.1'
info:
title: API Title
version: '1.0'
paths:
/hoge:
post:
responses:
"5XX":
description: その他サーバーエラー
"503":
description: サーバーがダウンしてる
- レスポンスのステータスコードにはワイルドカードとして
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
-
Operation Object に
callbacks
でコールバックを定義できる - コールバックは 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
- 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
- この例では、
"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: []
-
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
-
OpenAPI Object の
tags
で、タグの順序を定義できる -
tags
は Tag 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
-
OpenAPI Object の
webhooks
で 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: []
- 右上の [Authorize] というボタンをクリックすると、次のようなダイアログが表示される
- [Value] のところに適当な値を入れて [Authorize] ボタンをクリックする
- 値が入力された状態になるので、 [Close] でダイアログを閉じる
- [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
を指定した場合、in
とname
を追加で指定する必要がある-
in
には、APIキーをどのパラメータから渡すかを設定する-
query
,header
,cookie
のいずれかが指定できる
-
-
name
には、 API キーを設定するパラメータの名前を設定する- ここでは
in
をheader
にしているので、ヘッダーの名前を指定していることになる
- ここでは
-
security
security:
- apiKeyExample: []
-
security
には、実際にAPIリクエスト時に使用する認証方法を指定する -
security
は配列で指定し、配列の各要素は Security Requirement Object で指定する - Security Requirement Object は、キーに
securitySchemas
で定義した認証方法の識別名を、値には認証で使用する追加のパラメータをstring
の配列で渡す- ただし、追加のパラメータの要否は認証の種類によって異なっている
-
apiKey
の場合、追加のパラメータは必要ないので空の配列を渡しておけばいい - 認証の種類が
oauth2
かopenIdConnect
の場合、ここにスコープの配列を渡すことになる
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: []
## Method
POST
## Request URI
requestURI = /echo/hoge
queryString =
## Headers
...
authorization: Basic dGFybzpwYXNzd29yZA==
...
- 上記はHTTP認証の、特にBasic認証の場合の例になる
- HTTP認証の場合は
type
にhttp
を指定する - HTTP認証の場合、
schema
が必須指定となる-
schema
には使用するHTTP認証スキームの名前を指定する - この名前は IANA Authentication Scheme registry に登録されている名前のいずれかを使用する
- ここではBasic認証にするので
basic
と指定している
-
- Swagger UI から入力できる認証情報がユーザー名とパスワードになっており、Try itでリクエストを送ると、
authorization
ヘッダーにBasic認証の情報が載っていることがわかる
OpenID Connect
検証は以下のような構成で試した。
- WSL2 上の Docker で Keycloak と Swagger-UI を動かし、認可コードグラントフローでOIDCの認証を行いAPIアクセスを行う
- Keycloak と Swagger-UI の画面操作でブラウザを分けているのは、管理画面操作用のユーザーの認証情報がブラウザにあるとOIDC用のユーザーの認証とごっちゃになって面倒なので
- カッコ内の
8080
とかはポート番号
Keycloakの導入
認可サーバーである Keycloak を導入し、連携用のクライアントの登録と認証用のユーザーの作成を行う。
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] ボタンをクリック
- [Realm name] に
test-realm
と入力して [Create] ボタンをクリック
- メニューの左上の [Current Realm] が
test-realm
になっていることを確認して [Manage] > [Clients] をクリック
- [Creat client] ボタンをクリック
- 以下の要領で入力してクライアントを作成
- 記載のない項目はデフォルトのまま
項目 | 設定値 |
---|---|
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
が追加されているので、選択する
- [Credentials] タブを開き、 [Client Secret] の値を控える(後で Swagger-UI の方で設定する)
続いてユーザーを作成していく。
- メニューの [Users] を選択
- [Create new user] をクリック
- [Username] に
test-user
と入力して [Create] をクリック
- 作成された
test-user
の編集画面が開くので、 [Credentials] タブを開き [Set password] ボタンをクリック
- パスワードはとりあえず
password
にしておく - [Temporary] のチェックはオフにして [Save] をクリック
Swagger-UI の導入
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
を作成する - 内容は以下
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 は通るようにしないといけないので、そこはとりあえず全通しの設定を入れておく
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
以下任意のリクエストを受け付けてリクエストの情報をコンソールに書き出すコントローラを用意
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
にアクセスする- 前述のとおり、管理画面のセッションが残っているとややこしいので、そこがクリアできるなら何でもいい
- [Authorize] ボタンをクリックする
- ダイアログのスクロールが真ん中らへんに来ている場合は、一番上の [oidcExample (OAuth2, authorization_code)] まで移動する
- [client_id] に
swagger-client
、 [client_secret] に先ほど作成したクライアントシークレットを入力 - [Authorize] ボタンをクリック
- Keycloack のサインイン画面が開くので、
test-user
/password
と入力して [Sign In] ボタンをクリック
- 初回はユーザー情報の入力が求められるので、適当に入力して [Submit]
- 認証が完了したら [Close] でダイアログを閉じる
- 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 の場合、
type
はopenIdConnect
とする - 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 をダウンロードする
- まず、Swagger UI の GitHub リポジトリ をローカルに落とす
- 落とし方は git で clone でもいいし、 Releases からソースコードの zip をダウンロードするでもいい
dist フォルダを抜き出す
- ダウンロードした中に
dist
というフォルダがあるので、これを抜き出す
Tomcat に配備する
- 今回は Tomcat で動かすので
webapps
の下にdist
をコピーして、swagger
という名前にフォルダをリネームする -
swagger
フォルダの中は以下のような形でdist
の中身そのまま
ドキュメント化したいAPI定義ファイルを配置する
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
) の中に配置する
swagger-initializer.js を修正する
-
swagger-initializer.js
というファイルがあるので、これをテキストエディタで開く -
SwaggerUIBundle
関数の引数に渡しているurl
を、さきほど配置したsample.yml
に変更する
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
にブラウザでアクセスする
-
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
でいい
- JSON じゃないけど
- 起動したら
http://localhost:8080
にアクセスする
コード生成(OpenAPI Generator)
Swagger Codegen と OpenAPI Generator
- OpenAPI の API 定義ファイルからクライアントやサーバーのソースコードを自動生成するツールがいくつか存在する
- 有名なものとして Swagger Codegen や OpenAPI Generator がある
- もともとは Swagger Codegen として開発されていたが、OpenAPI 3.0 になかなか対応しないとか管理している会社による強引な修正がされるようになって有志がフォークしてできたのが OpenAPI Generator らしい
- このあたりの詳しい話は 平静を保ち、コードを生成せよ 〜 OpenAPI Generator誕生の背景と軌跡 〜 / gunmaweb34 - Speaker Deck にまとめられているので一読の価値あり
- 一応、現在は 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 イメージ が提供されているので、 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
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 を選択すればいい気がする
- resteasy に関する generator の出力については OpenAPI GeneratorでJAX-RS(RESTEasy)のサーバーサイドのソースコードを生成してみる - CLOVER🍀 で比較がされているの参考までに
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 クラス -
FooApi
がpaths
の定義を元に生成されたリソースクラスのファイルで、以下のようになっている(見やすくするため、一部改行を調整している)
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 の単純なアノテーション設定だけでは表現できない制限については無視される
- とはいえ、 JSON Schema で表現できる全ての制約が自動生成でカバーできるわけもなく、
- リクエストオブジェクトの
FooIdPostRequest
は以下のような感じ
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)を用意する
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
- すると、リソースクラスは以下のようにインタフェースで出力される
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
を設定する
useJakartaEe: true
- 生成されたリソースクラスは以下のようになる
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
という型で生成されるが、その内容が以下のようになっている
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 のコントローラが生成されている - 以下が、実際に出力されたソース(見やすくするため改行はいじっている)
/**
* 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);
}
}
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
<?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>
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/
説明
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.13.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<!-- 入力となる API 定義 -->
<inputSpec>src/main/resources/api.yml</inputSpec>
<!-- 使用するジェネレータ -->
<generatorName>jaxrs-spec</generatorName>
<!-- ジェネレータの設定 -->
<configOptions>
<!--
出力先ディレクトリ.
デフォルト値でいいので指定しておくとソースディレクトリとして認識される.
-->
<sourceFolder>src/gen/java</sourceFolder>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
- ソースの生成は、
openapi-generator-maven-plugin
のgenerate
ゴールを使用する- デフォルトで
generate-resources
フェーズに紐づくようになっている
- デフォルトで
-
configuration
で各種設定をしていく-
inputSpec
で、入力となる API 定義ファイルを指定する -
generatorName
で、使用するジェネレータを指定する -
configOptions
でジェネレータの設定を指定できる-
sourceFolder
はジェネレータが出力するソースコードのディレクトリパスを指定するためのオプション - jaxrs-spec ジェネレータのデフォルト値は
src/gen/java
- このオプションをデフォルト値でいいので設定しておくと、自動的に出力先のディレクトリがソースディレクトリとして認識されるようになる
- (デフォルト値があるのに未指定だと認識されないのがなんか気持ち悪い)
-
- そのほかの設定については 公式ドキュメント を参照
-
|-pom.xml
|-src/
| :
`-target/
|-generated-sources/
: |-openapi/
: |-pom.xml
|-README.md
`-src/
|-main/
| :
`-gen/
`-java/
`-org/openapitools/
|-api/
`-model/
-
generate
ゴールの結果はtarget/generated-sources
の下に出力される
<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
interfaceOnly: true
<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
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 プロジェクトを作成する
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
でローカルリポジトリにインストールしたクライアントモジュールを指している
- これは、先ほど
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>openapi-java-client</artifactId>
<version>1.0</version>
</dependency>
-
src/main/java/sandbox/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 の詳細へのリンクも記載されている
...
## 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 へリンクされており、そちらにさらに詳細が出力されている
...
### 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
のインスタンスは、Configuration
のgetDefaultApiClient
で取得できる -
Configuration.getDefaultApiClient
の実装を見に行くと、以下のようになっている
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 ( useJakartaEe が true の場合は 6.1.5) |
Jackson 2.17.1 |
webclient |
Spring WebClient 5.1.18 | Jackson 2.17.1 |
restclient |
Spring RestClient 6.1.6 | Jackson 2.17.1 |
resteasy |
Resteasy client 4.7.6 | Jackson 2.17.1 |
vertx |
VertX client 3.5.2. | Jackson 2.17.1 |
google-api-client |
Google API client 2.2.0 | Jackson 2.17.1 |
rest-assured |
rest-assured 5.3.2 | Gson 2.10.1 or 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 を使用する
- 拡張機能として OpenAPI (Swagger) Editor を入れている
- API ドキュメントは、 Swagger UI で表示できるようにする
- コード生成は OpenAPI Generator の Maven プラグインで実行できるようにする
- 基本は分割したままの管理だが、その他使用するツールが単一ファイルしかサポートしていない場合も想定して、結合する方法についても調べておく
- 自分が知っているものとしては、例えば Azure の API Management で API を OpenAPI の API 定義ファイルからロードすることができるが、その際は単一のファイルでないと読み込ませられない(はず)
実装
- 細かい説明はおいおいしていくとして、まずはざっとファイル分割した場合の構成と中身を記載する
openapi/
|-api.yml
|-components/
| `-book.yml
`-paths/
`-books.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"
openapi: '3.1.1'
info:
title: Book components
version: '1.0'
components:
schemas:
BookId:
type: integer
BookTitle:
type: string
BookPrice:
type: integer
Book:
type: object
properties:
id:
$ref: "#/components/schemas/BookId"
title:
$ref: "#/components/schemas/BookTitle"
price:
$ref: "#/components/schemas/BookPrice"
openapi: '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
- ルートとなるファイルと、分割したファイルで構成している
- 分割の方法については色々考えられるだろうが、ここではとりあえず
paths
やcomponents
など、 OpenAPI Object で定義されたプロパティ単位でフォルダを切ってみている -
paths
については「/books
で始まる API」のようにリソースのまとまりでファイルを分けてみている -
components
については、ひとまずリソース単位で分ける感じにしてみている - あくまで例の1つなので、適宜カスタマイズすればいい
- 分割の方法については色々考えられるだろうが、ここではとりあえず
ルートファイル
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 を参照
- [BUG] Error resolving complex Reference with path parameter #21058
- 一応こちらの Issue では Maven を使って実行時にパーセントエンコードへ切り替える方法も紹介されている
各分割ファイル
openapi: '3.1.1'
info:
title: Book components
version: '1.0'
components:
schemas:
BookId:
type: integer
BookTitle:
type: string
BookPrice:
type: integer
Book:
type: object
properties:
id:
$ref: "#/components/schemas/BookId"
title:
$ref: "#/components/schemas/BookTitle"
price:
$ref: "#/components/schemas/BookPrice"
- 各分割ファイルには本来
openapi
やinfo
は必要ないが、あえて記載するようにしている - その理由は、 VS Code で編集したときに補完を効かせるため
- これらが無いとただの YAML ファイル判定になって OpenAPI としての補完が効かないので
- なので
title
やversion
は正直適当でいい(実際、これらは使われないので)
-
components
をどこまで細かく定義すべきかは議論の余地はあると思うが、いったんここでは一番細かい単位まで定義してみた- 共通化を考えたら、個人的には結局はこの細かさに行きつく気はするが
API定義
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 をデプロイしているところと同じ場所に配置する
...
window.ui = SwaggerUIBundle({
url: "openapi/api.yml",
...
});
//</editor-fold>
};
-
swagger-initializer.js
の url に、ルートとなる API 定義ファイルへのパスを設定する
実行結果
- ちゃんと分割したファイルの内容も反映された形でドキュメントが生成できている
OpenAPI Generator (Maven プラグイン)
|-pom.xml
`-src/main/
|-java/
`-resources/
`-openapi/
|-api.yml
|-components/
`-paths/
-
src/main/resources/openapi
の下に API 定義ファイルを配置している
<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 で編集したとき
-
$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 でインストールできる
npm i -g @redocly/cli@latest
- Docker イメージも用意されているので、そちらでも可
- 今回は Docker でやってみる
-
api.yml
があるディレクトリまで移動して、以下のコマンドを実行する
docker run --rm -v `pwd`:/spec redocly/cli bundle /spec/api.yml -o /spec/result.yml
-
bundle
コマンドの引数にエントリとなるルートファイルを指定して、-o
で出力先ファイルを指定している - 以下が、実際に出力されたファイル
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
plugins {
id "java"
}
sourceCompatibility = 21
targetCompatibility = 21
compileJava.options*.encoding = "UTF-8"
repositories {
mavenCentral()
}
dependencies {
implementation "com.networknt:json-schema-validator:1.5.7"
}
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
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
==========
説明
implementation "com.networknt:json-schema-validator:1.5.7"
- com.networknt:json-schema-validator を依存関係に追加する
JsonSchemaFactory factory =
JsonSchemaFactory.getInstance(
SpecVersion.VersionFlag.V202012,
builder -> builder.metaSchema(OpenApi31.getInstance())
.defaultMetaSchemaIri(OpenApi31.getInstance().getIri())
);
- まずは
JsonSchemaFactory
を生成する - このあたりの実装は公式ドキュメントの記述をそのまま持ってきてるだけ
JsonSchema schema =
factory.getSchema(SchemaLocation.of(
"classpath:schema/sample.yml#/components/schemas/HogePostRequestBody"
));
- 次に検証で使用したい Schema 定義を読み込んで
JsonSchema
オブジェクトを取得する - このとき、
SchemaLocation.of
で JSON Pointer を利用した記法で読み込みたい Schema 定義を指定する- スラッシュは
~1
でエスケープが必要なので注意
- スラッシュは
- クラスパス上のファイルを指定する場合は
classpath:
で始める- ローカルファイルの場合は
file:///
で始める
- ローカルファイルの場合は
final Set<ValidationMessage> messages = schema.validate(json, InputFormat.JSON);
if (messages.isEmpty()) {
System.out.println("result: valid");
} else {
System.out.println("result: invalid");
System.out.println("messages:");
for (ValidationMessage message : messages) {
System.out.println(message.getMessage());
}
}
-
JsonSchema
のvalidate
メソッドで検証を実行できる - 結果は
ValidationMessage
のSet
で返される - 問題ない場合は空が返り、問題がある場合は違反内容ごとに
ValidationMessage
が設定されている - なお、試した限りでは YAML が分割されていても、問題なく
$ref
で指定した先のファイルまで見て検証してくれた
モックサーバー
起動
- 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 定義を読み込んで起動してみる。
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"}]
-
Prefer
で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つのファイルにまとめる - 元ページの説明だとディレクトリも分けるみたいな話が書かれているように読めるが、例がないので分からん
- たとえば
- ただし、使用するツールによっては大量に分割されたファイルに対応していないなどあるかもしれないので注意が必要
タグを使って整理する
- オペレーションにはタグを付与することができ、これでドキュメント上のソート順などを整理できる
参考
- What is OpenAPI? - OpenAPI Initiative
- OpenAPIとは|Swaggerとの違いや役立つツールを紹介 | AeyeScan
- OpenAPI Specification v3.1.1
- Getting Started | OpenAPI Documentation
- OpenAPI Tooling
- Visual Studio CodeでOpenAPI (Swagger) Editorを使用|Web制作プラスジャムのなかやすみ
- JSON Schema reference
- OpenAPITools/openapi-generator: OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)