Codableについて色々まとめた[Swift4.x]


# はじめに

Swift 4対応の一つ、Codableについていくつか疑問点を実験しつつ、日本語でまとめました。

元記事は以下ですが、たぶん元記事よりも補足しています。
[Encoding and Decoding Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types)

4/9追記: Swift4.1(というかXcode 9.3)で追加されたkeyEncodingStrategy, keyDecodingStrategyと、それに合わせてCodingKeyプロトコルについて追記しました。

おまけ: PlaygroudとかでJSON表示するだけのコード何回も書いてたのでgistにおきました。良かったらご利用ください。
[PrintEncodable.swift](https://gist.github.com/ha1f/569dc76f9c67e07b32481c606724ac56)

# Codable, Encodable, Decodable

```swift
typealias Codable = Decodable & Encodable
```

DecodableとEncodableどっちもにconformしたのがCodable

クラスにも構造体にも適用可能です。

# 自動Codable

String, Int, Double, Date, Data, URLは既にCodable
Array, Dictionary, Optional も中身がCodableであればCodable
Codableなプロパティのみから構成された構造体はCodable

よって、以下ののように書くだけでCodableとして扱える

```swift
struct Landmark: Codable {
    var name: String
    var foundingYear: Int
}
```

# CodingKeys

エンコード/デコードのキーが一致しない時に用いる。補完とかされないけれど、構造体内部にこの名前で定義すると使える。

case名をプロパティ名、rawValueをエンコード結果のフィールド名として定義する

case自体を省略するとエンコード・デコードされない。この時、Decodableにするにはdefault valueが必要。

```swift
import Foundation

struct Coordinate: Codable {
    var latitude: Double
    var longitude: Double
    var elevation: Double = 0 // default value
    
    enum CodingKeys: String, CodingKey {
        case latitude = "another_key"
        case longitude
    }
}

let data = try! JSONEncoder().encode(Coordinate(latitude: 0, longitude: 0, elevation:0))
print(String(data: data, encoding: String.Encoding.utf8)!)

```

> {"another_key":0,"longitude":0}

Swift 4.0ではcamelCase <-> snake_caseの変換のためだけにでも定義する必要があったが、Swift 4.1で追加されたDecoderの `keyDecodingStrategy` を使うと省略できる (参考: [Swift 4.1 improves Codable with keyDecodingStrategy](https://www.hackingwithswift.com/articles/52/swift-4-1-improves-codable-with-keydecodingstrategy))

詳しくはJSONEncoerの節で紹介します。

# Manual Implementation

データがネストしているときなど、エンコードのフォーマットがデータ構造と一致しない時は、自分で `encode()` メソッドを実装する必要がある。

フィールドの定義は同じようにCodingKeyを使う

```swift
import Foundation

struct Coordinate {
    var latitude: Double
    var longitude: Double
    var elevation: Double
    
    enum CodingKeys: String, CodingKey {
        case latitude
        case longitude
        case additionalInfo
    }
    
}

extension Coordinate: Encodable {
    private enum AdditionalInfoKeys: String, CodingKey {
        case elevation
    }
    
    func encode(to encoder: Encoder) throws {
        // containerはvarにしておく
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(latitude, forKey: .latitude)
        try container.encode(longitude, forKey: .longitude)
        
        var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        try additionalInfo.encode(elevation, forKey: .elevation)
    }
}

let data = try! JSONEncoder().encode(Coordinate(latitude: 0, longitude: 0, elevation:0))
print(String(data: data, encoding: String.Encoding.utf8)!)
```

> {"additionalInfo":{"elevation":0},"longitude":0,"latitude":0}

Decodable

```swift
extension Coordinate: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        latitude = try values.decode(Double.self, forKey: .latitude)
        longitude = try values.decode(Double.self, forKey: .longitude)
        
        let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
    }
}
```

# JSONEncoder, JSONDecoder

Encodableなオブジェクトを実際にJSONにエンコードするには、JSONEncoder等を使う。

```swift
let encoder = JSONEncoder()
let data = try! encoder.encode(Coordinate(latitude: 0, longitude: 0, elevation:0))
print(String(data: data, encoding: String.Encoding.utf8)!)
```

基本的にはこれだけでOK
utf8でData型にエンコード/デコードされる。
その他、以下のような設定がある。

### outputFormatting

#### .prettyPrinted, .sortedKeys

[.sortedKeys](https://developer.apple.com/documentation/foundation/jsonencoder.outputformatting/2919670-sortedkeys)にするとキー順でソートされる
[.prettyPrinted](https://developer.apple.com/documentation/foundation/jsonencoder.outputformatting/2919679-prettyprinted)にすると人間の読みやすいJSON形式になる

```swift
encoder.outputFormatting = .prettyPrinted
```

> {
  "additionalInfo" : {
    "elevation" : 0
  },
  "longitude" : 0,
  "latitude" : 0
}

#### 複数まとめて設定したりもできる

```swift
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
```

### dateEncodingStrategy

`.custom` だとclosureで指定できる。その他、 `.iso8601`, `.millisecondsSince1970` などがある
デフォルトでは `.deferredToDate`

### dataEncodingStrategy

`.custom` だとclosureで指定できる。その他、`.deferredToData` などがある
デフォルトだと `.base64`

### nonConformingFloatEncodingStrategy

Floatでinfinity等があった時の扱いを指定する

### keyEncodingStrategy (Xcode 9.3で追加)

snake_caseやcamelCaseを変換する設定。以下の三種類がある。デフォルトは `useDefaultKeys`

- [useDefaultKeys](https://developer.apple.com/documentation/foundation/jsonencoder.keyencodingstrategy/2949124-usedefaultkeys)
- [convertToSnakeCase](https://developer.apple.com/documentation/foundation/jsonencoder.keyencodingstrategy/2949134-converttosnakecase)
- [custom([CodingKey] -> CodingKey)](https://developer.apple.com/documentation/foundation/jsonencoder.keyencodingstrategy/2949120-custom)

但し、あくまでルート以下のkeyに対する設定で、ネストされたCodingKeyはそれぞれの方式に従います。
useDefaultKeysは読んで字のごとくそのままです。

#### convertToSnakeCase

最も一般的であろう、「SwiftでcamelCase、JSONでsnake_case」を用いる場合は、以下のようにします。

```swift
struct CaseSensitive: Codable {
    var camelCase: Int
}

let object = CaseSensitive(camelCase: 17)

let jsonEncoder = JSONEncoder()
jsonEncoder.keyEncodingStrategy = .convertToSnakeCase
let json = try jsonEncoder.encode(object)
print(String(data: json, encoding: String.Encoding.utf8)!)

let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let decoded = try jsonDecoder.decode(CaseSensitive.self, from: json)
print(decoded)
```

> {"camel_case":17}
CaseSensitive(camelCase: 17)

#### custom

キーの変換を `[CodingKey] -> CodingKey` なクロージャで指定する。

[ドキュメントのサンプル](https://developer.apple.com/documentation/foundation/jsonencoder.keyencodingstrategy/2949120-custom)で紹介されていた例は、以下の様なエンコード。

```swift
struct A: Codable {
    var value: Int
    var b: B
    
    struct B: Codable {
        var value: Int
        var c: C
        
        struct C: Codable {
            var value: Int
        }
    }
}
```

> {
     "a.value": 1,
     "b": {
         "a.b.value": 2,
         "c": {
            "a.b.c.value": 3
         }
     }
 }
 
まず、以下のような `AnyKey` があると便利らしい（後続のCodingKeyプロトコルの節でもう少し書きます）

```swift
/// An implementation of CodingKey that's useful for combining and transforming keys as strings.
struct AnyKey: CodingKey {
    var stringValue: String
    var intValue: Int?
    
    init?(stringValue: String) {
        self.stringValue = stringValue
        self.intValue = nil
    }
    
    init?(intValue: Int) {
        self.stringValue = String(intValue)
        self.intValue = intValue
    }
}
```

そしてエンコードは以下のように定義すると、さっきの出力が得られる。

```swift
let a: A = A(value: 1, b: .init(value: 2, c: .init(value: 3)))

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.keyEncodingStrategy = .custom { keys in
    if keys.last!.stringValue == "value" {
        return AnyKey(stringValue: "a." + keys.map { key in
            key.stringValue
        }.joined(separator: "."))!
    } else {
        return keys.last!
    }
}
print(String(data: try! encoder.encode(a), encoding: .utf8)
```

keysにはネストされたCodingKeyのアレイが渡される。
keys.last!に対して必要な変換をしてを返すのが基本だが、
今回のように親オブジェクトのプロパティに依存させることもできる。

# CodingKeyプロトコル

上の `AnyKey` 構造体の通り、CodingKeyは `stringValue: String`, `intValue: Int?` を持つ。実際のキーには `stringValue` の値が使われる。

JSONのキーはECMAの仕様によれば、常にStringなのでStringは必須っぽい。

![cca9bd6b0d53a747f2f9a8c63abbd9ca-1.png](https://qiita-image-store.s3.amazonaws.com/0/77778/c3372fd3-36d1-e9f2-26d5-b5f0851fc182.png "cca9bd6b0d53a747f2f9a8c63abbd9ca-1.png")


(出典: [The JSON Data Interchange Syntax](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf))

EncoderはJSONだけではないので`intValue` が用意されているんだと思うが、具体的な用途は不明。


# 継承クラスのエンコード

```swift
class Animal {
    var name: String = "foo"
}

class Dog: Animal, Codable {
    var dogName: String = "dog"
}

let data = try! JSONEncoder().encode(Dog())
print(String(data: data, encoding: String.Encoding.utf8)!)
```

> {"dogName":"dog"}

継承元クラスのプロパティはエンコードされません。

```swift
enum CodingKeys: String, CodingKey {
    case name
    case dogName
}
```

もコンパイルエラーになります。

```swift
class Animal: Codable {
    var name: String = "animal"
}

class Dog: Animal {
    var dogName: String = "dog"
}

let data = try! JSONEncoder().encode(Dog())
print(String(data: data, encoding: String.Encoding.utf8)!)
```

> {"name":"animal"}

親クラスをCodableにすると、今度は親クラスのプロパティしかエンコードされません

よって、このような場合は以下のように手動で定義する必要があります。

```swift
class Animal {
    var name: String = "animal"
}

class Dog: Animal, Encodable {
    var dogName: String = "dog"
    
    private enum CodingKeys: String, CodingKey {
        case name
        case dogName
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(dogName, forKey: .dogName)
        try container.encode(name, forKey: .name)
    }
}

let data = try! JSONEncoder().encode(Dog())
print(String(data: data, encoding: String.Encoding.utf8)!)
```

> {"dogName":"dog","name":"animal"}

# getterのエンコード

```swift
class Animal: Codable {
    var name: String = "animal"
    var name2: String {
        return name
    }
}

let data = try! JSONEncoder().encode(Animal())
print(String(data: data, encoding: String.Encoding.utf8)!)
```

> {"name":"animal"}

getterはエンコードされません。

```swift
enum CodingKeys: String, CodingKey {
    case name
    case name2
}
```

は `note: CodingKey case 'name2' does not match any stored properties` としてコンパイルエラーになります。

よってこの場合も手動で実装する必要があります。

# EncodingContainer

自分でencodingを実装する場合に使うものです。

|型|役割|
|:-:|:-:|
|KeyedEncodingContainer|辞書|
|UnkeyedEncodingContainer|アレイ|
|SingleValueEncodingContainer|単一値|

```swift
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(dogName, forKey: .dogName)
    try container.encode(name, forKey: .name)
}
```

単一値の場合は以下のように使います。ただし、JSONのエンコード結果が単一値であるとエラーになる（一番外側はアレイもしくは辞書でなければいけない）ので、注意が必要です。

```swift
extension RealmOptional: Encodable {
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value)
    }
}
```

ネストさせる場合（辞書やアレイの場合）は、

```swift
// 辞書
container.nestedContainer

// アレイ
container.nestedUnkeyedContainer
```

などでcontainerを取得できます。

# Codableなenum

rawvalueが入ります

```swift
enum FooBar: String, Codable {
    case foo
    case bar
}

struct Hoge: Encodable {
    let value = FooBar.foo
}

let data = try! JSONEncoder().encode(Hoge())
print(String(data: data, encoding: String.Encoding.utf8)!)
```

> {"value":"foo"}

enumに限らず、[RawRepresentable](https://developer.apple.com/documentation/swift/rawrepresentable) に準拠している場合は、標準実装で、RawValueに対してエンコード/デコードされます。