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

今回はOpenAPIで標準とされているデータ型について。
メイントピックは、**Null** の扱いについてです。


# どんなデータ型が許容されているのか
[OpenAPI Specification#Data Types](https://github.com/OAI/OpenAPI-Specification/blob/v3.0.3-dev/versions/3.0.2.md#dataTypes)を見てみると、以下になります。

| 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）](https://ja.wikipedia.org/wiki/ISO_8601)で定義されている日付形式が表現可能です。

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

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

OASとして定義されているフォーマットは以上なのですが、
`format` には他にも自由に値を設定することができます。
`email` や `uuid`, `uri`, `hostname`, `ipv4`, `ipv6` など、OASの仕様として策定されているもの以外の任意の形式が指定可能です。
これらは、サードパーティのツールがバリデーションを自由に行えるようにするために許容されています。

ちなみにスキーマを検証する機能などを提供する[committee](https://github.com/interagent/committee) では、`format`までの検証はしてくれません。
例えば `int32` に 2147483648 以上を指定してもエラーにはなりません。

# さらに詳細なフォーマット
## Mixed Types
`string` または `integer` みたいな OR 指定ができる。

```yaml
oneOf:
  - type: string
  - type: integer
```

## Range
`minimum`, `maximum` キーワードで値の範囲指定ができる。

```yaml
type: integer
minimum: 1
maximum: 20
```

閾値は含まれるので、これは `1 ≤ n ≤ 20` を表している。
含みたくない場合は、 `exclusiveMinimum`, `exclusiveMaximum` キーワードが使える。

```yaml
type: integer
minimum: 1
maximum: 20
exclusiveMinimum: true
```

こうすると `1 < n ≤ 20` となる。

## Multiples

nの倍数のような指定。
この場合、10, 20, 30, 0, -10, -20 などが有効な値となる。
負の値は指定できない。
一応小数も指定できるがあまりおすすめはしない。

```yaml
type: integer
multipleOf: 10
```

## Length
`string` の文字数制限。
空文字 `""` については、 `minLength` を省略した場合は有効な値として扱われる。

```yaml
type: string
minLength: 3
maxLength: 20
```

## Pattern
正規表現。
JavaScriptの [ECMA 262](https://www.ecma-international.org/ecma-262/5.1/#sec-7.8.5) をベースとしている。

```yaml
zip_code:
  type: string
  pattern: '^\d{3}-\d{4}$'
```

# Booleanについて
`true` または `false` のみが `boolean` として有効な値。
`""`, `"true"`, `0`, `null` などは無効。

# Arrays
配列。

```yaml
type: array
items:
  type: string
```

ネストすることもできる。

```yaml
# [ [1, 2], [3, 4] ]
type: array
items:
  type: array
  items:
    type: integer
```

包含する値の型指定も可能。

```yaml
type: array
items:
  oneOf:
    - type: string
    - type: integer
```

サイズ指定。

```yaml
type: array
items:
  type: integer
minItems: 1
maxItems: 10
```

重複する値は許容しない。

```yaml
type: array
items:
  type: integer
uniqueItems: true
```

# File について
2.0であった `file` タイプは、3.0では削除されている。
代わりに `string` の `format` を用いて表現する。

```yaml
type: string
format: binary  # binary file contents
```

or

```yaml
type: string
format: byte    # base64-encoded file contents
```

# Nullの扱い
OpenAPI3.0は、`Null`をデータ型としてサポートしていませんが、
`nullable` プロパティを使うことで、ある程度表現することはできます。

```yaml
age:
  type: integer
  nullable: true
```

<img width="267" alt=" 2019-12-03 9.30.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/916748a7-d099-4c2b-c7b8-e4b9ba2fff02.png">

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

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

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

```yaml
User:
  description: "ユーザー"
    type: "object"
      properties:
        name:
          description: "名前"
          type: "string"
        company:
          description: "所属する会社"
          type: "object"
          properties:
            name:
              description: "社名"
              type: "string"
          required:
            - "name"
      required:
        - "name"
```

<img width="446" alt=" 2019-12-03 10.05.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/b3ab01a2-d566-b688-70aa-7be8c0c0ef30.png">

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

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

or

{
  "name": "sakuraya"
}
```


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

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

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

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

```yaml
company:
  nullable: true
```

<img width="444" alt=" 2019-12-03 10.12.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/6dd59ff6-54bb-ece1-8fbe-2d8cba194607.png">

変化はありません。

`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”
](https://github.com/OAI/OpenAPI-Specification/issues/1368#issuecomment-487605182)
- [Response types using Nullable/oneOf are invisible in swagger-ui (OpenApi3)](https://github.com/RicoSuter/NSwag/issues/2071)

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

- `type: null` と書く人がいるが、これは仕様として完全に間違い。
- `type: "object"` に対して `nullable: true` をアタッチするのは OpenAPI として正しい書き方。
- しかし上の書き方だと SwaggerUI には反映されない。
- SwaggerUI のバグじゃないとかという主張がある。
- nullable と他のキーワードがバッティングしていて解釈が多様にできてしまうという OAS の仕様の不完全さが原因だから改善しようという議論がある。

こんなところです。


# be required, or be nullable
先の正しい書き方があるにせよ、SwaggerUI上でうまく表示されないのは現場では大きな混乱を招きます。
company が null を返す可能性のあるAPIを以下に記述してみます。

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

<img width="467" alt=" 2019-12-03 10.25.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/495b8737-0da1-eb69-9a88-99d66dd254f4.png">

company の中の name が必須であるため、余計 null である可能性を考えにくくなってしまっているかとも思います。
定義（yamlファイル）を見ればわかるとはいえ、利便性にかけます。

ここで考えたいのは、このような存在しない可能性がある Object 型のプロパティは、APIとしてどう設計するのがそもそも適切かということです。

選択肢は3つあります。


#### 1. null にする

```json
{
  "name": "hoge",
  "company": null
}
```

#### 2. 空にする


```json
{
  "name": "hoge",
  "company": {}
}
```

#### 3. そもそも含めない

```json
{
  "name": "hoge"
}
```

広く一般的な議論が様々あると思いますが、 
結論から言うと OpenAPI に関しては、1のようなケースを視覚的にわかりやすく表現するのは手間です（※不可能ではない）。

3は `required` とするだけでできます。2は特に何もする必要がありません。

1を無理やり表現するとしたら下記のような感じです。

```yaml
company:
  oneOf:
    - $ref: "#/components/schemas/Company"
    - $ref: "#/components/schemas/NullValue"
```

```yaml
NullValue:
  description: this respresents `null`
    not:
      anyOf:
        - type: string
        - type: number
        - type: integer
        - type: boolean
        - type: array
            items: {}
        - type: object
```

⬇️

<img width="841" alt=" 2019-12-05 22.06.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/f166d57f-f7f2-4455-d943-e3fe92a7c66b.png">


めっちゃ大変ですね？

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

#  さてどうするか

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

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



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

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