[Swift] あなたのDictionaryは本当にCodableなの？

# 概要

言いたいこと: **「思った通りに`Dictionary`をencode/decodeできないことがあるよ！」**

## 前提

この記事では分かりやすさのためJSONへの変換を考えます。特に断りがなければ`encoder`は次のものとします。また、`printJSON`という関数も定義しておきます。

```Swift:JSONEncoder.swift
let encoder = JSONEncoder()
encoder.outputFormatting = [
  .prettyPrinted,
  .sortedKeys,
  .withoutEscapingSlashes,
]

func printJSON<T>(_ object: T) throws where T: Encodable {
  print(String(data: try encoder.encode(object), encoding: .utf8)!)
}
```


# そもそも`Dictionary`は`Codable`になるの？

"[Dictionary | Apple Developer Documentation](https://developer.apple.com/documentation/swift/dictionary)"のConforms Toの項目には、

> Decodable
>  - Conforms when Key conforms to Decodable and Value conforms to Decodable.
> Encodable
>  - Conforms when Key conforms to Encodable and Value conforms to Encodable.

ということが書かれています。
`Codable`は`Encodable & Decodable`のことなので、`Key`と`Value`両方が`Codable`に適合していれば、`Dictionary<Key, Value>`も`Codable`になるということになります。


## 例: `Key`も`Value`も`String`の場合

`String`はもともと`Codable`なので、`Dictionary<String, String>`も`Codable`になるはずです。
実際のコードをみてみましょう:

```Swift:EncodeStringDictionary.swift
let dictionary: Dictionary<String, String> = [
  "key0": "value0",
  "key1": "value1",
]

try printJSON(dictionary)
```

とても簡単。これを実行すると次のようにJSONが表示されるはずです。

```JSON
{
  "key0" : "value0",
  "key1" : "value1"
}
```

期待通りですね。


# 独自の`Key`にしてみよう！

`Key`が`Codable`なら(`Value`も`Codable`である限り)`Dictionary`も`Codable`になるとのことでした。そこで、`Codable`に適合する独自の型を`Key`にしてみましょう。

```Swift:MyKey.swift
enum MyKey: String, Codable {
  case key0
  case key1
}
```

すごく単純な`enum`の`MyKey`です[^myKey]。

[^myKey]: `String`を`RawValue`とする`RawRepresentable`とすることで、`Codable`の実装を自分でせずに済みます。

では、この`MyKey`を`Key`とする`Dictionary`をencodeしてみましょう！
コードは次のようになります：

```Swift:EncodeMyKeyDictionary.swift
enum MyKey: String, Codable {
  case key0
  case key1
}

let dictionary: Dictionary<MyKey, String> = [
  .key0: "value0",
  .key1: "value1",
]

try printJSON(dictionary)
```

結果は…

```JSON
[
  "key0",
  "value0",
  "key1",
  "value1"
]
```

**！？**


`Key`と`Value`がフラットに並んだ配列になっているんですけど！
`Dictionary<String, String>`のときと同じ結果になることを期待したんですけど！


## なぜフラットな配列になるのか？

答えは[公式ドキュメント](https://developer.apple.com/documentation/swift/dictionary/2893562-encode)に書かれています: 

> If the dictionary uses String or Int keys, the contents are encoded in a keyed container. Otherwise, the contents are encoded as alternating key-value pairs in an unkeyed container.
> 
> (YOCKOW拙訳) `String`か`Int`をキーとして用いている場合、ディクショナリの中身はキーで紐づけられたコンテナにエンコードされます。それ以外の場合、キーで紐づけられないコンテナにキーと値のペアが交互にエンコードされます。

すなわち、上記の"EncodeMyKeyDictionary.swift"は、`Key`が`String`でも`Int`でもないため、キーと値が交互に並ぶ配列としてエンコードされてしまうのです[^dictionary-encodable]。

[^dictionary-encodable]: 実際の実装はGitHubで見ることができます: https://github.com/apple/swift/blob/4dab4c235b975f9b092dac504f0546bf3a5d54e1/stdlib/public/core/Codable.swift#L5523-L5560

フラットな配列は`JSONDecoder`を使えば`Dictionary<MyKey, String>`にデコードできるのですが、`Dictionary`なのにエンコードされると配列になるというのは気持ち悪いですね。`Foundation`以外のライブラリやSwift以外の言語と連携するときは気をつけなければいけません。


# やっぱり`Dictionary`はオブジェクト(連想配列)にしたい

世界中にそう思っている人がいるはずで、Swift JIRAにも該当の項目があります: [SR-7788](https://bugs.swift.org/browse/SR-7788)。
そこで私めも[コメントさせていただいた](https://bugs.swift.org/browse/SR-7788?focusedCommentId=57191&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-57191)わけなのですが、これは設計段階でのミスに思えます。`Dictionary`を`Codable`にしたかったら**`Key`が適合すべきは`Codable`ではなく[`CodingKey`](https://developer.apple.com/documentation/swift/codingkey)**のはずなのです。しかし、今から変えようとするとAPIが破壊的変更となってしまうため、実現可能性はかなり低いでしょう。

いくつか選択肢はありますが、[SwiftCodableDictionary](https://github.com/YOCKOW/SwiftCodableDictionary)を利用するという手があります。`Dictionary`の代わりにこのモジュールの`CodableDictionary`を利用することで、期待通りのエンコード/デコードができるはずです。

```Swift:WithSwiftCodableDictionary.swift
import Foundation
import CodableDictionary

enum MyKey: String, CodableDictionaryKey {
  case key0
  case key1
}

let dictionary: CodableDictionary<MyKey, String> = [
  .key0: "value0",
  .key1: "value1",
]

try printJSON(dictionary) // -> 期待通り
```


# まとめ

以上、[SwiftCodableDictionary](https://github.com/YOCKOW/SwiftCodableDictionary)の宣伝でした(え？。