Codableに対応したMessagePackのエンコーダー/デコーダーを作った際に得た知見

弊社では、一部のサービスで[MessagePack](https://msgpack.org/)を利用しています。
Swift製のMessagePackエンコーダー/デコーダーのライブラリは存在するのですが、Swift4から利用できるCodableに対応した、いい感じのライブラリがなかったので、Codableに対応したMessagePackのエンコーダー/デコーダーを作ってみました。

https://github.com/hirotakan/MessagePacker

この記事では、MessagePackについて軽く説明した後、Codableに対応したカスタムエンコーダー/デコーダーを実装する上で得た知見を共有できればと思っています。


## MessagePackとは

[MessagePack](https://msgpack.org/)は性能を重視したバイナリベースのシリアライズ形式です。
JSONのように汎用的なシリアライズ形式でありつつ、バイナリベースで高速です。


### MessagePackの特徴

- シリアライズ/デシリアライズがとても高速
- シリアライズされたデータのサイズが小さい
- フォーマット定義（IDL）が不要
- ストリーム処理できる

> http://frsyuki.hatenablog.com/entry/20080816/p1 より


### MessagePackの仕様

- 基本的にFirst byteがデータ型を表し、後ろにデータが続く形式
- バイトオーダーはビックエンディアン

詳しい仕様はGithubに上がっていますので興味がある方はこちらを参照ください。
https://github.com/msgpack/msgpack/blob/master/spec.md


## Codableとは

Swift4から採用されたプロトコルで、Codableに準拠するとエンコード/デコードが可能であるということを宣言でき、コンパイラが必要なコードを自動で補完してくれます。
Codableは、EncodableとDecodableのtypealiasとして定義されています。

```swift
typealias Codable = Encodable & Decodable

protocol Encodable {
    func encode(to encoder: Encoder) throws
}

protocol Decodable {
    init(from decoder: Decoder) throws
}
```


### Codableのメリット

Codableに準拠している型は、Codableに対応したエンコーダー/デコーダーを利用するとエンコード/デコード処理が容易になります。

Swiftには標準で `JSONEncoder` , `JSONDecoder` が存在します。

```swift
let input = Landmark(name: "Mojave Desert")
// Landmark -> JSON
let data = try! JSONEncoder().encode(input)
// JSON -> Landmark
let landmark = try! JSONDecoder().decode(Landmark.self, from: data)
```

また、シリアライズ形式が変わった場合は、エンコーダー/デコーダーを変えるだけで対応可能です。

```swift
let input = Landmark(name: "Mojave Desert")
// Landmark -> MessagePack
let data = try! MessagePackEncoder().encode(input)
// MessagePack -> Landmark
let landmark = try! MessagePackDecoder().decode(Landmark.self, from: data)
```


## カスタムエンコーダー/デコーダーの基本
ここからは、カスタムエンコーダー/デコーダーを実装するための基本的な部分を見ていきます。
エンコーダーとデコーダーは似たような形になるので、ここではエンコーダーを例にあげます。


### エンコーダーの実装に必要なプロトコル

エンコーダーを実装するにあたり、次のプロトコルに準拠する型を定義する必要があります。

- [Encoder](https://developer.apple.com/documentation/swift/encoder)
- [CodingKey](https://developer.apple.com/documentation/swift/codingkey)
- [KeyedEncodingContainer](https://developer.apple.com/documentation/swift/keyedencodingcontainer)([KeyedEncodingContainerProtocol](https://developer.apple.com/documentation/swift/keyedencodingcontainerprotocol))
- [UnkeyedEncodingContainer](https://developer.apple.com/documentation/swift/unkeyedencodingcontainer)
- [SingleValueEncodingContainer](https://developer.apple.com/documentation/swift/singlevalueencodingcontainer) 

`KeyedEncodingContainer` は、正確にいうとプロトコルではありません。
これは、`KeyedEncodingContainer` が準拠している `KeyedEncodingContainerProtocol`がassociatedtypeを持つため、Type Erasureが使われています。
`KeyedEncodingContainer`の型を実装する際は、実質 `KeyedEncodingContainerProtocol` に準拠する形になります。

```swift
public protocol Encoder {
    public var codingPath: [CodingKey] { get }

    public var userInfo: [CodingUserInfoKey : Any] { get }

    public func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey

    public func unkeyedContainer() -> UnkeyedEncodingContainer

    public func singleValueContainer() -> SingleValueEncodingContainer
}
```

`Encoder` のメソッドはエンコード対象の型により呼ばれるメソッドが変わります。
よって、使用される `XXXEncodingContainer` プロトコルは各型と次のような対応関係にあります。

| Protocol | Type |
|:---|:---|
| KeyedEncodingContainer | Dictionary |
| UnkeyedEncodingContainer | Array |
| SingleValueEncodingContainer | Optional, Bool, String, Double, Float, Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64 And Custom type|

これらのプロトコルに準拠した型を定義し、エンコード処理を書いていくのが基本的な実装になります。

また、 必須ではないですが、標準の[JSONEncoder](https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/JSONEncoder.swift)や[PropertyListEncoder](https://github.com/apple/swift/blob/6e7051eb1e38e743a514555d09256d12d3fec750/stdlib/public/Darwin/Foundation/PlistEncoder.swift)を見るとエンコードしたデータを保持しておく `XXXStrage` も必要になってくるのがわかります。


詳しい実装内容は、それらのEncoderや[MessagePackEncoder](https://github.com/hirotakan/MessagePacker/blob/master/Sources/MessagePackEncoder.swift)の実装を見てみてください。

## カスタムエンコーダー/デコーダー実装時に注意が必要な点

### エンコーダーの対象外な型（Date/Data型など）

現在エンコーダーで対応できる型は次のようになっています。

| Type |
|:-----|
|Bool|
|String|
|Double|
|Float|
|Int|
|Int8|
|Int16|
|Int32|
|Int64|
|UInt|
|UInt8|
|UInt16|
|UInt32|
|UInt64|

（要素がCodableに準拠している場合の `Dictionary` , `Array` , `Optional`）

ですが、Date/Data型はCodableに準拠しているのでエンコード/デコード可能です。
なぜでしょうか？

実装をみてみましょう。

```swift
extension Date : Codable {
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.timeIntervalSinceReferenceDate)
    }
}
```

> https://github.com/apple/swift/blob/6e7051eb1e38e743a514555d09256d12d3fec750/stdlib/public/Darwin/Foundation/Date.swift#L290-L301


`Date`型の `encode`では、`timeIntervalSinceReferenceDate`がエンコード対象となっています。
`timeIntervalSinceReferenceDate`は、 `TimeInterval`型で `TimeInterval` は `Double`型のエイリアスなので、実質 `Double`として処理されます。

`Data`型の実装もみてみましょう。

```swift
extension Data : Codable {
    var container = encoder.unkeyedContainer()
    try withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
        try container.encode(contentsOf: buffer)
    }
}
```

> https://github.com/apple/swift/blob/d2da2080b709ca6f37d428b188d4d61d93bcc1d9/stdlib/public/Darwin/Foundation/Data.swift#L2793-L2823

`Data`型では、バイト配列に変換され、`UnkeyedEncodingContainer` が使用されています。
よって`Data`型は、 `[UInt8]` として処理されます。

これが意図した結果であればいいのですが、違う処理に変えたい場合は一工夫必要です。

`JSONEncoder` では `Date/Data`型にキャストし、それぞれ適切な処理に通しています。

```swift
fileprivate func box(_ date: Date) throws -> NSObject {
    ...
}

fileprivate func box(_ data: Data) throws -> NSObject {
    ...
}

fileprivate func box_<T : Encodable>(_ value: T) throws -> NSObject? {
    if T.self == Date.self {
        return try self.box((value as! Date))
    } else if T.self == Data.self {
        return try self.box((value as! Data))
    }
}
```

### DictionaryのキーがString or Int型じゃない場合

Swiftでは、`Hashable` に準拠していれば `Dictionary` のキーとして利用することができます。
この場合エンコード処理では、`String` or `Int` の場合と挙動が変わってきます。

```swift
public func encode(to encoder: Encoder) throws {
    if Key.self == String.self {
      var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
      for (key, value) in self {
        let codingKey = _DictionaryCodingKey(stringValue: key as! String)!
        try container.encode(value, forKey: codingKey)
      }
    } else if Key.self == Int.self {
      var container = encoder.container(keyedBy: _DictionaryCodingKey.self)
      for (key, value) in self {
        let codingKey = _DictionaryCodingKey(intValue: key as! Int)!
        try container.encode(value, forKey: codingKey)
      }
    } else {
      var container = encoder.unkeyedContainer()
      for (key, value) in self {
        try container.encode(key)
        try container.encode(value)
      }
    }
  }
}
```

> https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift.gyb#L1820-L1857

`String` or `Int` の場合は、 `KeyedEncodingContainer` が使われているのに対し、それ以外では `UnkeyedEncodingContainer` が使用されています。
この辺を理解していないと意図した挙動をしない場合があるかもしれません。


### superEncoder

`KeyedEncodingContainer` と `UnkeyedEncodingContainer` に準拠した型には、 `superEncoder` メソッドを実装する必要があります。
これは継承関係にある型をエンコードする際に利用します。

```swift
class Animal: Codable {
    var legCount: Int

    private enum CodingKeys: String, CodingKey {
        case legCount
    }

    init(legCount: Int) {
        self.legCount = legCount
    }

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

class Dog: Animal {
    var name: String

    private enum CodingKeys: String, CodingKey {
        case name
    }

    required init(legCount: Int, name: String) {
        self.name = name
        super.init(legCount: legCount)
    }

    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        // ↓↓↓↓ ここ
        let superEncoder = container.superEncoder()
        try super.encode(to: superEncoder)
    }
}
```

自分が実装する際に `superEncoder`がどのような場面で利用されるのかよくわからなかったので、？？？って感じだったのですが、この[stack overflow](https://stackoverflow.com/questions/44553934/using-decodable-in-swift-4-with-inheritance)のページで理解することができました:clap::clap::clap:


## さいごに

この記事が、カスタムエンコーダー/デコーダーを実装する際の手助けになれば幸いです。

[MessagePacker](https://github.com/hirotakan/MessagePacker)を実装するにあたり、[JSONEncoder](https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/JSONEncoder.swift)と[MoreCodable](https://github.com/tattn/MoreCodable)の実装をとても参考にさせていただきました。ありがとうございます:pray:

もしiOSでMessagePackを利用する機会があれば、是非 [MessagePacker](https://github.com/hirotakan/MessagePacker)を検討してみてください:wave: