120
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RESTful APIAdvent Calendar 2019

Day 2

そのフィールド、nullable にしますか、requiredにしますか

Last updated at Posted at 2019-12-05

今回は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 に関しては bytebinary の他に、ISO 8601(RFC3339)で定義されている日付形式が表現可能です。

基本形式: 20191129T203637+0900
拡張形式: 2019-11-29T20:36:37+09:00

password はいまいちよく分かりません。

OASとして定義されているフォーマットは以上なのですが、
format には他にも自由に値を設定することができます。
emailuuid, 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では削除されている。
代わりに stringformat を用いて表現する。

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
 2019-12-03 9.30.22.png

以下のようなユースケースを考えてみます。

  • ユーザーがいる。
  • ユーザーは必ず名前をもっている。
  • ユーザーは会社に所属していることがある。(必須ではない)

これらをOpenAPIのスキーマで表すとこうなります。

User:
  description: "ユーザー"
    type: "object"
      properties:
        name:
          description: "名前"
          type: "string"
        company:
          description: "所属する会社"
          type: "object"
          properties:
            name:
              description: "社名"
              type: "string"
          required:
            - "name"
      required:
        - "name"
 2019-12-03 10.05.54.png

特に気になるところはありません。
レスポンスはこうなります。

{
  "name": "sakuraya"
  "company": "itmono"
}

or

{
  "name": "sakuraya"
}

しかし世の中のAPIには、こう返すものもあります。

{
  "name": "sakuraya"
  "company": null
}

キーは必須として存在しているが、値がないことを null として表す仕様です。

これは OpenAPI のスキーマとしてどう表現できるでしょうか?
company を nullable にしてみます。

company:
  nullable: true
 2019-12-03 10.12.24.png

変化はありません。

nullablevalue に対するオプションです。

nullable: Allows sending a null value for the defined schema. Default value is false.

対して objectpropertyvalue の集合のため、扱いが違うのかなーとも思いました。

An object is a collection of property/value pairs

が、いろいろ調べてみると結構根深い問題でした。

リンク先の議論からざっくり噛み砕くと、

  • 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 になる可能性があることを読み取って正しく実装できるでしょうか?

 2019-12-03 10.25.45.png

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

⬇️

 2019-12-05 22.06.46.png

めっちゃ大変ですね?

全てのプリミティブな型に対する否定を設定し、間接的に null にしかなりえないスキーマ NullValue なるものを錬金して、 Company または NullValue であることを宣言することで、視覚的にもスキーマ的にも nullable なオブジェクトを表現しています。

さてどうするか

RESTの仕様を標準化しようとしている OpenAPI が現状においてこのような位置付けである以上、
2または3の設計にしておくのが無難ではないかと思います。

そもそも nullable がサポートされたのも3.0になってからで、
2.0 では null はもっと扱いが難しいものでした。
単なるプロパティでさえも、値が null ならそもそもキーとして含めるなというOASの強い意志を感じます。

nullable キーワードに関してはコミュニティでも激しく議論されてますが、
標準仕様として、これまでの互換性を考慮して、今後もかなり難航しそうな匂いを感じます 🤔

言語によっても null の扱いが微妙に異なることを考えれば、
JSONのキーの有無で判定できる方がシンプルで汎用的な設計になるじゃないでしょうか。知らんけど。

120
58
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?