[Swift] CodingKey とは何か?

JSON 等の encoder/decoder でよく使われる CodingKey を少し詳しく調べてみました。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterery beta 3
  • Xcode 13 beta3
  • iOS 15 beta
MEMO
以下のサンプルコードは、Playground で実行して確認することができます。

CodingKey の使われる場所

JSON 等の encoder/decoder をカスタマイズしようとすると登場してきます。


enum CodingKeys: Int, CodingKey {
  case type1
  case type2
}

上記のように定義すると、struct や class の encode/decode 時のキーとして使用することができます。

この時に、準拠する必要のある "CodingKey" とは? という点を説明します。

CodingKey の定義

encode/decode でキーとして使用することのできるタイプ (Protocol) として定義されています。

Apple のドキュメントは、こちら

どうして、CodingKey に準拠するとキーとして使えるのか? 準拠しないとキーとして使えないのか? を最初のスタート地点としました。

MEMO
encode/decode 時に言えることですが、わかりやすいので、JSON 表記を使って説明していきます。

調査に使う enum

以下のように enum を定義して、調べてみました。


// CodingKey に準拠
enum CodingKeys: CodingKey {
    case type1
    case type2
    case type3
}

CodingKey protocol

CodingKey というプロトコルは、var intValue:Int? と var stringValue:String というプロパティを要求します。

いずれも、それを key として Dictionary を扱うことができると書かれているので、ユニークな Int や ユニークなString を提供することがあるということです。

上記で定義した enum は、特にプロトコルで要求するプロパティを実装していませんが、コンパイルできます。つまりデフォルトの実装が用意されているようです。

以下で、どのような実装か見てみます。

CodingKey に準拠した enum


enum CodingIntKeys: CodingKey {
    case type1
    case type3
    case type2
}

let key1 = CodingKeys.type1
print(key1.intValue)           // nil
print(key1.stringValue)        // type1
let key2 = CodingKeys.type2
print(key2.intValue)           // nil
print(key2.stringValue)        // type2

上記の実行結果を見ると、intValue には、nil が stringValue には、enum の enumeration case が設定されるようです。

enum 内では、同一の名称を持つ enumeration case は、定義できませんので、この stringValue は dictionary の key としても使うことができます。

しかし、intValue には、いずれの enum 変数に対しても nil が返されるので、このままでは、intValue を dictionary の key には使えないようです。

CustomStringConvertible, CustomDebugStringConvertible

面白いことに、CodingKey は、CustomeStringConvertible と CustomDebugStringConvertible に準拠しています。

この2つの protocol は それぞれ、var description: String と var debugDescription: String というプロパティを要求しています。

ですので、以下のように、CodingKey に準拠した enum の変数をわかりやすい String 型に変換することができます。

CodingKey に準拠した型の description/debugDescription


enum CodingKeys: CodingKey {
    case type1
    case type2
    case type3
}

let key1 = CodingKeys.type1

print(key1.description)        // CodingKeys(stringValue: "type1", intValue: nil)
print(key1.debugDescription)   // CodingKeys(stringValue: "type1", intValue: nil)

description/debugDescription 共に同じ String が返されてきました。

Encode への影響を確認してみる

Encode 時にどのように使用されているかを確認してみます。

まずは、デフォルト実装を使って JSON に encode してみます。

デフォルト実装を使った JSON への encode


struct MyStruct: Encodable {
    var myValue: Int
    enum CodingKeys: CodingKey {
        case myValue
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(myValue, forKey: .myValue)
    }
}

let data = MyStruct(myValue: 5)
let json = try! JSONEncoder().encode(data)
print(String(data: json, encoding: .utf8)!)
// output : {"myValue":5}

CodingKey で用意されているデフォルト実装の stringValue がキー値として使われていそうです。

独自実装した CodingKey を使って、JSON へ encode

CodingKey でデフォルト実装が提供されている stringValue を独自実装で上書きしてみます。


struct MyStruct: Encodable {struct MyStruct: Encodable {
    var myValue: Int
    enum CodingKeys: CodingKey {
        case myValue
        var stringValue: String {
            switch self {
            case .myValue:
                return "modifiedMyValue"
            }
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(myValue, forKey: .myValue)
    }
}

let data = MyStruct(myValue: 5)
let json = try! JSONEncoder().encode(data)
print(String(data: json, encoding: .utf8)!)
// output : {"modifiedMyValue":5}

デフォルト実装を上書きすることで、自分で実装した stringValue がキー値として使用されることが確認できました。

不明点:intValue は何のため?

CodingKey だけに準拠すると intValue は、nil が返されるのですが、CodingKey と Int に準拠されると別のデフォルト実装が提供されるようになります。

CodingKey と Int に準拠した enum

CodingKey と Int に準拠させてチェックしてみます。


enum CodingIntKeys: Int, CodingKey {
    case type1
    case type3
    case type2
}

let keyi1 = CodingIntKeys.type1
let keyi2 = CodingIntKeys.type2
print(keyi1.intValue)           // Optional(0)
print(keyi1.stringValue)        // type1
print(keyi2.intValue)           // Optional(1)
print(keyi2.stringValue)        // type2

intValue は、enum 内での定義順が返されるようです。(0スタートです)

試しに順序を変えてみると、intValue も変わります。


enum CodingIntKeys: Int, CodingKey {
    case type3
    case type1
    case type2
}

let keyi1 = CodingIntKeys.type1
print(keyi1.intValue)        // Optional(1)
print(keyi1.stringValue)     // "type1"

デフォルト実装で intValue がどのように提供されるかは見えてきましたが、CodingKey として encode/decode にどのように関与しているかは、残念ながらわかりませんでした。

ご存じでしたら、教えてください。

まとめ:CodingKey に準拠させるということ

CodingKey に準拠させるということ
  • stringValue プロパティが用意され、encode/decode 時のキーとして使用される
  • デフォルト実装をオーバーライドすることで、使用するキー値を変更することができる

今回説明したことをベースに、encode/decode 時のキーを変更することが可能です。以下の記事で説明しています。

Swift[Swift] JSON データの扱い方

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です