LoginSignup
7

More than 1 year has passed since last update.

posted at

updated at

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

概要

言いたいこと: 「思った通りに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 

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
What you can do with signing up
7