Swift Archival & Serialization
01 何が問題だったのか
Swift 3 までの時点では、アーカイブやシリアライゼーションのAPIは Foundation の NSCoding / NSJSONSerialization / NSPropertyListSerialization に依存しており、Objective-C の動的な世界観に合わせて設計されていました。そのため、Swift らしい型の使い方と噛み合わない箇所が多く残っていました。
struct や enum を NSCoding に載せられない
NSCoding は @objc プロトコルであり、適合できるのは class 型だけでした。Swift では小さな struct や enum でモデルを表現するのがイディオマティックですが、そうしたモデルをアーカイブしたい場合は、
- Swift のよさを諦めて
classにする - 「実際のモデル型」と「アーカイブ用の互換レイヤ型」を二重に持つ
のどちらかを強いられていました。Swift ネイティブの値型をそのままアーカイブできる仕組みがなかったということです。
JSON / plist のシリアライゼーションが型安全でない
JSONSerialization や PropertyListSerialization は、結果を Any([String: Any] や [Any] を含む構造)として返します。実際に使う側は、キャストと Optional アンラップを重ねて、以下のように値を取り出すことになります。
// 典型的な JSONSerialization の使い方(Swift 3まで)
let data: Data = ...
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
guard let farm = json,
let name = farm["name"] as? String,
let location = farm["location"] as? [String: Any],
let latitude = location["latitude"] as? Double,
let longitude = location["longitude"] as? Double,
let animals = farm["animals"] as? [Int] else {
// 何が壊れていたかは分からない
throw MyError.invalid
}
各階層ごとにキャストが必要で、どこでどう失敗したのかも分かりにくく、Swift の強い型付けの恩恵を受けられませんでした。サードパーティのJSON変換ライブラリもさまざまなアプローチを試みていましたが、いずれもボイラープレートや型安全性とのトレードオフを抱えていました。
NSCoding の設計がSwiftに合わない点
NSCoding には、Swiftのモデルに移植するとさらに扱いにくくなる特徴もありました。
- キーが
Stringなので、キー名の typo をコンパイラが検出できない。 - プリミティブの符号化・復号は、型ごとに異なるメソッド名(
encodeInt32:forKey:/decodeInt32ForKey:など)を使い分ける必要がある。 - 継承時の
superの符号化が「同じ辞書に平坦に詰める」慣習になっており、キーが衝突する恐れがある。また、親子で異なるコンテナ形状(配列 vs 辞書)を使うこともできない。
Swift 側で値型を自然に扱えて、かつ JSON / plist といった外部表現とも型安全に往復できる、新しいアーカイブ・シリアライゼーションの仕組みが必要でした。
02 どのように解決されるのか
Swift 標準ライブラリに、アーカイブとシリアライゼーションのための新しいプロトコル群を導入します。値型(struct / enum)を含む任意の型が、型安全な形で外部表現との変換に参加できるようになります。これは3段階からなる計画の第1段で、プロトコルの基盤を規定するものです。具体的なエンコーダ・デコーダAPI(JSONEncoder / PropertyListEncoder など)と NSCoding との相互運用は、続く SE-0167 で提案されます。Swift 4.0 で導入されました。
Encodable / Decodable / Codable
型は Encodable / Decodable に適合することで、このシステムに参加します。両方を合わせた typealias Codable = Encodable & Decodable も用意されます。
public struct Location: Codable {
public let latitude: Double
public let longitude: Double
}
public enum Animal: Int, Codable {
case chicken = 1
case dog
case turkey
case cow
}
public struct Farm: Codable {
public let name: String
public let location: Location
public let animals: [Animal]
}
Codable に適合した型の値は、(SE-0167 で導入される)エンコーダ・デコーダによってデータと相互変換できます。
let farm = Farm(
name: "Old MacDonald's Farm",
location: Location(latitude: 51.621648, longitude: 0.269273),
animals: [.chicken, .dog, .cow, .turkey]
)
let payload: Data = try JSONEncoder().encode(farm)
do {
let decoded = try JSONDecoder().decode(Farm.self, from: payload)
let coordinates = "\(decoded.location.latitude), \(decoded.location.longitude)"
} catch {
// 復号エラー
}
コンパイラによる自動合成
すべてのプロパティが Encodable / Decodable であれば、encode(to:) と init(from:) の実装はコンパイラが自動生成します。上の Location / Farm / Animal はいずれもメソッド本体を書いていませんが、これで動作します。自動生成で不足な場合だけ、手で実装を書けば置き換えられます。
CodingKey によるキーの型付け
プロパティを辞書状に符号化する際のキーは、String ではなく CodingKey プロトコルに適合した専用の型(通常は enum)で表します。これにより、キーの typo はコンパイル時に検出でき、補完も効きます。
public protocol CodingKey {
var stringValue: String { get }
init?(stringValue: String)
var intValue: Int? { get }
init?(intValue: Int)
}
プロパティ名とキー名を一致させたい場合は、CodingKeys という名前の enum をコンパイラが自動生成してくれます。キー名だけ変えたい場合は、String をrawValueとして持つ enum を手で書きます。
struct Article: Codable {
let id: Int
let title: String
let body: String
// プロパティ名とJSONのキー名が違う場合だけ手で書く
private enum CodingKeys: String, CodingKey {
case id
case title
case body = "content"
}
}
キーに Int を持たせることもでき、エンコーダは必要に応じて String と Int のどちらを使うかを選べます。
コンテナという抽象
Encoder / Decoder は「コンテナ」を提供し、型はそのコンテナを通じて値を読み書きします。コンテナは3種類あります。
- keyed container: キーつきで値を並べる(辞書に相当)
- unkeyed container: キーなしで順に並べる(配列に相当)
- single value container: 単一の値をそのまま入れる
たとえば Location を手で実装するなら、keyed container を使います。
public struct Location: Codable {
public let latitude: Double
public let longitude: Double
private enum CodingKeys: CodingKey {
case latitude
case longitude
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(latitude, forKey: .latitude)
try container.encode(longitude, forKey: .longitude)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
latitude = try container.decode(Double.self, forKey: .latitude)
longitude = try container.decode(Double.self, forKey: .longitude)
}
}
Bool / Int / Double / String などのプリミティブ型には専用のオーバーロードが用意されており、型安全に書けます。任意の Encodable / Decodable を受け付ける汎用版もあります。
Animal のような raw value 付き enum は single value container を使うのが自然で、こちらも自動合成の対象です。実体として、[.chicken, .dog, .cow] は [1, 2, 4] として符号化されます。
ネスト
外部フォーマットに合わせて、コンテナ内部にさらにコンテナを入れ子にすることもできます。JSON API が {"id": ..., "properties": {"name": ..., "timestamp": ...}} のような形を要求するとき、Swift 側のフラットな型にマッピングするのに役立ちます。
struct Record: Codable {
let id: Int
let name: String
let timestamp: Double
private enum Keys: CodingKey {
case id
case properties
}
private enum PropertiesKeys: CodingKey {
case name
case timestamp
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Keys.self)
try container.encode(id, forKey: .id)
var nested = container.nestedContainer(keyedBy: PropertiesKeys.self, forKey: .properties)
try nested.encode(name, forKey: .name)
try nested.encode(timestamp, forKey: .timestamp)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
id = try container.decode(Int.self, forKey: .id)
let nested = try container.nestedContainer(keyedBy: PropertiesKeys.self, forKey: .properties)
name = try nested.decode(String.self, forKey: .name)
timestamp = try nested.decode(Double.self, forKey: .timestamp)
}
}
継承
class の継承を扱うための仕組みも用意されます。NSCoding の慣習では親子が同じ辞書に値を詰めるためキーが衝突し得ましたが、新APIでは、コンテナ上で superEncoder() / superDecoder() を呼んで親用の入れ子コンテナを取り出し、そこに super を符号化します。
public class MyCodable: SomethingCodable {
public override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
// ... 自分のプロパティを符号化
// super は入れ子のコンテナに符号化する
try super.encode(to: container.superEncoder())
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// ... 自分のプロパティを復号
try super.init(from: container.superDecoder())
}
}
こうすることで、親と子が別々のコンテナ形状を選べるようになり、キー衝突の心配もなくなります。
実行時コンテキスト
Encoder / Decoder は userInfo: [CodingUserInfoKey: Any] を公開しており、エンコード・デコード時に呼び出し側から任意のコンテキストを渡せます。「プライベートなフィールドを含めるかどうか」など、同じ型でも状況に応じて振る舞いを変えたいケースで使います。
プリミティブ型への Codable 適合
Bool / Int / Double / String など標準のプリミティブ型はすべて Codable に適合します。加えて、RawValue がプリミティブ型の RawRepresentable には Codable のデフォルト実装が提供されるため、enum MyKind: Int, Codable {} と書くだけで符号化・復号ができるようになります。
まとめ
Codable によって、Swift の値型を含む任意のモデルが、CodingKey という型付きのキーと3種類のコンテナを通じて外部表現と往復できるようになります。多くのケースでは適合を宣言するだけで十分で、独自のキー名やネスト構造が必要な場合にだけ手で実装を差し込みます。これがアーカイブ・シリアライゼーション新APIの土台であり、次段の SE-0167 で具体的なJSON / plistエンコーダ・デコーダが提案されます。