今回はOpenAPIで標準とされているデータ型について。
メイントピックは、Null の扱いについてです。
どんなデータ型が許容されているのか
OpenAPI Specification#Data Typesを見てみると、以下になります。
type | format | Comments |
---|---|---|
integer | int32 | signed 32 bits |
integer | int64 | signed 64 bits (a.k.a long) |
number | float | |
number | double | |
string | ||
string | byte | base64 encoded characters |
string | binary | any sequence of octets |
boolean | ||
string | date | As defined by full-date - RFC3339 |
string | date-time | As defined by date-time - RFC3339 |
string | password | A hint to UIs to obscure input. |
プリミティブな型としては、 integer
, number
, string
, boolean
の4種類のみです。
さらに format
というプロパティを指定すると、値の詳細なフォーマットを定義することができます。
int32
, int64
, float
, double
あたりは言わずもがなですね。
string
に関しては byte
と binary
の他に、ISO 8601(RFC3339)で定義されている日付形式が表現可能です。
基本形式: 20191129T203637+0900
拡張形式: 2019-11-29T20:36:37+09:00
password
はいまいちよく分かりません。
OASとして定義されているフォーマットは以上なのですが、
format
には他にも自由に値を設定することができます。
email
や uuid
, uri
, hostname
, ipv4
, ipv6
など、OASの仕様として策定されているもの以外の任意の形式が指定可能です。
これらは、サードパーティのツールがバリデーションを自由に行えるようにするために許容されています。
ちなみにスキーマを検証する機能などを提供するcommittee では、format
までの検証はしてくれません。
例えば int32
に 2147483648 以上を指定してもエラーにはなりません。
さらに詳細なフォーマット
Mixed Types
string
または integer
みたいな OR 指定ができる。
oneOf:
- type: string
- type: integer
Range
minimum
, maximum
キーワードで値の範囲指定ができる。
type: integer
minimum: 1
maximum: 20
閾値は含まれるので、これは 1 ≤ n ≤ 20
を表している。
含みたくない場合は、 exclusiveMinimum
, exclusiveMaximum
キーワードが使える。
type: integer
minimum: 1
maximum: 20
exclusiveMinimum: true
こうすると 1 < n ≤ 20
となる。
Multiples
nの倍数のような指定。
この場合、10, 20, 30, 0, -10, -20 などが有効な値となる。
負の値は指定できない。
一応小数も指定できるがあまりおすすめはしない。
type: integer
multipleOf: 10
Length
string
の文字数制限。
空文字 ""
については、 minLength
を省略した場合は有効な値として扱われる。
type: string
minLength: 3
maxLength: 20
Pattern
正規表現。
JavaScriptの ECMA 262 をベースとしている。
zip_code:
type: string
pattern: '^\d{3}-\d{4}$'
Booleanについて
true
または false
のみが boolean
として有効な値。
""
, "true"
, 0
, null
などは無効。
Arrays
配列。
type: array
items:
type: string
ネストすることもできる。
# [ [1, 2], [3, 4] ]
type: array
items:
type: array
items:
type: integer
包含する値の型指定も可能。
type: array
items:
oneOf:
- type: string
- type: integer
サイズ指定。
type: array
items:
type: integer
minItems: 1
maxItems: 10
重複する値は許容しない。
type: array
items:
type: integer
uniqueItems: true
File について
2.0であった file
タイプは、3.0では削除されている。
代わりに string
の format
を用いて表現する。
type: string
format: binary # binary file contents
or
type: string
format: byte # base64-encoded file contents
Nullの扱い
OpenAPI3.0は、Null
をデータ型としてサポートしていませんが、
nullable
プロパティを使うことで、ある程度表現することはできます。
age:
type: integer
nullable: true
以下のようなユースケースを考えてみます。
- ユーザーがいる。
- ユーザーは必ず名前をもっている。
- ユーザーは会社に所属していることがある。(必須ではない)
これらをOpenAPIのスキーマで表すとこうなります。
User:
description: "ユーザー"
type: "object"
properties:
name:
description: "名前"
type: "string"
company:
description: "所属する会社"
type: "object"
properties:
name:
description: "社名"
type: "string"
required:
- "name"
required:
- "name"
特に気になるところはありません。
レスポンスはこうなります。
{
"name": "sakuraya"
"company": "itmono"
}
or
{
"name": "sakuraya"
}
しかし世の中のAPIには、こう返すものもあります。
{
"name": "sakuraya"
"company": null
}
キーは必須として存在しているが、値がないことを null として表す仕様です。
これは OpenAPI のスキーマとしてどう表現できるでしょうか?
company を nullable にしてみます。
company:
nullable: true
変化はありません。
nullable
は value
に対するオプションです。
nullable: Allows sending a null value for the defined schema. Default value is false.
対して object
は property
と value
の集合のため、扱いが違うのかなーとも思いました。
An object is a collection of property/value pairs
が、いろいろ調べてみると結構根深い問題でした。
- Reference objects don't combine well with “nullable”
- Response types using Nullable/oneOf are invisible in swagger-ui (OpenApi3)
リンク先の議論からざっくり噛み砕くと、
-
type: null
と書く人がいるが、これは仕様として完全に間違い。 -
type: "object"
に対してnullable: true
をアタッチするのは OpenAPI として正しい書き方。 - しかし上の書き方だと SwaggerUI には反映されない。
- SwaggerUI のバグじゃないとかという主張がある。
- nullable と他のキーワードがバッティングしていて解釈が多様にできてしまうという OAS の仕様の不完全さが原因だから改善しようという議論がある。
こんなところです。
be required, or be nullable
先の正しい書き方があるにせよ、SwaggerUI上でうまく表示されないのは現場では大きな混乱を招きます。
company が null を返す可能性のあるAPIを以下に記述してみます。
User:
description: "ユーザー"
type: "object"
properties:
name:
description: "名前"
type: "string"
company:
nullable: true
$ref: "#/components/schemas/Company"
required:
- "name"
- "company"
Company:
description: "所属する会社"
type: "object"
properties:
name:
description: "社名"
type: "string"
required:
- "name"
SwaggerUIの出力はこうなります。
サーバーサイドチームから送られてきたこの仕様をクライアントチームが見て、company が null になる可能性があることを読み取って正しく実装できるでしょうか?
company の中の name が必須であるため、余計 null である可能性を考えにくくなってしまっているかとも思います。
定義(yamlファイル)を見ればわかるとはいえ、利便性にかけます。
ここで考えたいのは、このような存在しない可能性がある Object 型のプロパティは、APIとしてどう設計するのがそもそも適切かということです。
選択肢は3つあります。
1. null にする
{
"name": "hoge",
"company": null
}
2. 空にする
{
"name": "hoge",
"company": {}
}
3. そもそも含めない
{
"name": "hoge"
}
広く一般的な議論が様々あると思いますが、
結論から言うと OpenAPI に関しては、1のようなケースを視覚的にわかりやすく表現するのは手間です(※不可能ではない)。
3は required
とするだけでできます。2は特に何もする必要がありません。
1を無理やり表現するとしたら下記のような感じです。
company:
oneOf:
- $ref: "#/components/schemas/Company"
- $ref: "#/components/schemas/NullValue"
NullValue:
description: this respresents `null`
not:
anyOf:
- type: string
- type: number
- type: integer
- type: boolean
- type: array
items: {}
- type: object
⬇️
めっちゃ大変ですね?
全てのプリミティブな型に対する否定を設定し、間接的に null にしかなりえないスキーマ NullValue
なるものを錬金して、 Company または NullValue であることを宣言することで、視覚的にもスキーマ的にも nullable なオブジェクトを表現しています。
さてどうするか
RESTの仕様を標準化しようとしている OpenAPI が現状においてこのような位置付けである以上、
2または3の設計にしておくのが無難ではないかと思います。
そもそも nullable
がサポートされたのも3.0になってからで、
2.0 では null はもっと扱いが難しいものでした。
単なるプロパティでさえも、値が null ならそもそもキーとして含めるなというOASの強い意志を感じます。
nullable キーワードに関してはコミュニティでも激しく議論されてますが、
標準仕様として、これまでの互換性を考慮して、今後もかなり難航しそうな匂いを感じます 🤔
言語によっても null の扱いが微妙に異なることを考えれば、
JSONのキーの有無で判定できる方がシンプルで汎用的な設計になるじゃないでしょうか。知らんけど。