LoginSignup
22
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-03

概要

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

前提

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

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)!)
}

そもそもDictionaryCodableになるの?

"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.

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

例: KeyValueStringの場合

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

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

try printJSON(dictionary)

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

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

期待通りですね。

独自のKeyにしてみよう!

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

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

すごく単純なenumMyKeyです1

では、このMyKeyKeyとするDictionaryをencodeしてみましょう!
コードは次のようになります:

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

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

try printJSON(dictionary)

結果は…

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

!?

KeyValueがフラットに並んだ配列になっているんですけど!
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拙訳) StringIntをキーとして用いている場合、ディクショナリの中身はキーで紐づけられたコンテナにエンコードされます。それ以外の場合、キーで紐づけられないコンテナにキーと値のペアが交互にエンコードされます。

すなわち、上記の"EncodeMyKeyDictionary.swift"は、KeyStringでもIntでもないため、キーと値が交互に並ぶ配列としてエンコードされてしまうのです2

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

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

世界中にそう思っている人がいるはずで、Swift JIRAにも該当の項目があります: SR-7788
そこで私めもコメントさせていただいたわけなのですが、これは設計段階でのミスに思えます。DictionaryCodableにしたかったらKeyが適合すべきはCodableではなくCodingKeyのはずなのです。しかし、今から変えようとするとAPIが破壊的変更となってしまうため、実現可能性はかなり低いでしょう。

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

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の宣伝でした(え?。


  1. StringRawValueとするRawRepresentableとすることで、Codableの実装を自分でせずに済みます。 

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

22
7
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
22
7