概要
言いたいこと: 「思った通りにDictionary
をencode/decodeできないことがあるよ!」
前提
この記事では分かりやすさのためJSONへの変換を考えます。特に断りがなければencoder
は次のものとします。また、printJSON
という関数も定義しておきます。
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"の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
になるはずです。
実際のコードをみてみましょう:
let dictionary: Dictionary<String, String> = [
"key0": "value0",
"key1": "value1",
]
try printJSON(dictionary)
とても簡単。これを実行すると次のようにJSONが表示されるはずです。
{
"key0" : "value0",
"key1" : "value1"
}
期待通りですね。
独自のKey
にしてみよう!
Key
がCodable
なら(Value
もCodable
である限り)Dictionary
もCodable
になるとのことでした。そこで、Codable
に適合する独自の型をKey
にしてみましょう。
enum MyKey: String, Codable {
case key0
case key1
}
すごく単純なenum
のMyKey
です1。
では、このMyKey
をKey
とするDictionary
をencodeしてみましょう!
コードは次のようになります:
enum MyKey: String, Codable {
case key0
case key1
}
let dictionary: Dictionary<MyKey, String> = [
.key0: "value0",
.key1: "value1",
]
try printJSON(dictionary)
結果は…
[
"key0",
"value0",
"key1",
"value1"
]
!?
Key
とValue
がフラットに並んだ配列になっているんですけど!
Dictionary<String, String>
のときと同じ結果になることを期待したんですけど!
なぜフラットな配列になるのか?
答えは公式ドキュメントに書かれています:
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
でもないため、キーと値が交互に並ぶ配列としてエンコードされてしまうのです2。
フラットな配列はJSONDecoder
を使えばDictionary<MyKey, String>
にデコードできるのですが、Dictionary
なのにエンコードされると配列になるというのは気持ち悪いですね。Foundation
以外のライブラリやSwift以外の言語と連携するときは気をつけなければいけません。
やっぱりDictionary
はオブジェクト(連想配列)にしたい
世界中にそう思っている人がいるはずで、Swift JIRAにも該当の項目があります: SR-7788。
そこで私めもコメントさせていただいたわけなのですが、これは設計段階でのミスに思えます。Dictionary
をCodable
にしたかったら**Key
が適合すべきはCodable
ではなくCodingKey
**のはずなのです。しかし、今から変えようとするとAPIが破壊的変更となってしまうため、実現可能性はかなり低いでしょう。
いくつか選択肢はありますが、SwiftCodableDictionaryを利用するという手があります。Dictionary
の代わりにこのモジュールのCodableDictionary
を利用することで、期待通りのエンコード/デコードができるはずです。
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の宣伝でした(え?。
-
String
をRawValue
とするRawRepresentable
とすることで、Codable
の実装を自分でせずに済みます。 ↩ -
実際の実装はGitHubで見ることができます: https://github.com/apple/swift/blob/4dab4c235b975f9b092dac504f0546bf3a5d54e1/stdlib/public/core/Codable.swift#L5523-L5560 ↩