概要
言いたいこと: 「思った通りにDictionary
をencode/decodeできないことがあるよ!」
前提
この記事では分かりやすさのためJSONへの変換を考えます。特に断りがなければencoder
は次のものとします。また、printJSON
という関数も定義しておきます。
letencoder=JSONEncoder()encoder.outputFormatting=[.prettyPrinted,.sortedKeys,.withoutEscapingSlashes,]funcprintJSON<T>(_object:T)throwswhereT:Encodable{print(String(data:tryencoder.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
になるはずです。
実際のコードをみてみましょう:
letdictionary:Dictionary<String,String>=["key0":"value0","key1":"value1",]tryprintJSON(dictionary)
とても簡単。これを実行すると次のようにJSONが表示されるはずです。
{"key0":"value0","key1":"value1"}
期待通りですね。
独自のKey
にしてみよう!
Key
がCodable
なら(Value
もCodable
である限り)Dictionary
もCodable
になるとのことでした。そこで、Codable
に適合する独自の型をKey
にしてみましょう。
enumMyKey:String,Codable{casekey0casekey1}
すごく単純なenum
のMyKey
です1。
では、このMyKey
をKey
とするDictionary
をencodeしてみましょう!
コードは次のようになります:
enumMyKey:String,Codable{casekey0casekey1}letdictionary:Dictionary<MyKey,String>=[.key0:"value0",.key1:"value1",]tryprintJSON(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
を利用することで、期待通りのエンコード/デコードができるはずです。
importFoundationimportCodableDictionaryenumMyKey:String,CodableDictionaryKey{casekey0casekey1}letdictionary:CodableDictionary<MyKey,String>=[.key0:"value0",.key1:"value1",]tryprintJSON(dictionary)// -> 期待通り
まとめ
以上、SwiftCodableDictionaryの宣伝でした(え?。
String
をRawValue
とするRawRepresentable
とすることで、Codable
の実装を自分でせずに済みます。 ↩実際の実装はGitHubで見ることができます: https://github.com/apple/swift/blob/4dab4c235b975f9b092dac504f0546bf3a5d54e1/stdlib/public/core/Codable.swift#L5523-L5560 ↩