Swift Digest
SE-0167 | Swift Evolution

Swift Encoders

Proposal
SE-0167
Authors
Itai Ferber, Michael LeHew, Tony Parker
Review Manager
Doug Gregor
Status
Implemented (Swift 4.0)

01 何が問題だったのか

SE-0166 では、Encodable / Decodable / Codable といったアーカイブとシリアライゼーションのための基盤プロトコルが規定されました。しかし、それだけでは実際に値を JSON や property list として書き出したり読み込んだりすることはできません。基盤プロトコルに適合させただけの型を、実用的な外部フォーマットと往復させる仕組みが別途必要でした。

また、Swift らしい Codable の世界観と、Foundation が長く提供してきた NSCoding の世界観をどうつなぐかという問題もありました。既存のコードベースには NSKeyedArchiver / NSKeyedUnarchiver を使った NSCoding ベースのアーカイブが数多く存在し、それらを少しずつ Codable に移していく現実的な経路が求められていました。

具体的なエンコーダ・デコーダがない

Codable に適合した型があっても、SE-0166 自体は Encoder / Decoder プロトコルを定義するだけで、それを満たす具体的なエンコーダ・デコーダは提供されていませんでした。各フォーマット(JSON や property list)について、次のようなことを一貫した方法で扱えるAPIが必要でした。

  • Swift 値と Data を直接やり取りするトップレベルのAPI
  • 日付やバイナリデータなど、フォーマットごとに表現の選択肢がある値についてのエンコード戦略
  • JSON であれば無限大や NaN のように、フォーマットが直接表現できない浮動小数点値の扱い

エラーの語彙が不揃い

サードパーティも含めてさまざまなエンコーダ・デコーダが作られることを前提にすると、エンコード・デコード時に投げられるエラーの語彙が実装ごとにバラバラだと、利用側は実装ごとにエラーハンドリングを書き分ける必要が出てきます。「値がフォーマットに載らない」「要求した型と実際の型が食い違う」「データが壊れている」「キーや値が見つからない」といった典型的な失敗を、共通の語彙で表せる必要がありました。

NSCoding との相互運用

既存の NSKeyedArchiver / NSKeyedUnarchiverNSCoding に適合したオブジェクトしか扱えませんでした。新しく Codable にした値型を同じアーカイブに混ぜて入れたい、あるいは Foundation の Date / URL / UUID といった値型を Objective-C 側の NSDate / NSURL / NSUUID と互換のある形で書き出したい、という要求に応える仕組みは用意されていませんでした。

02 どのように解決されるのか

SE-0166 で導入された Codable の仕組みに対し、Foundation 側から次の3つを追加で提供します。Swift 4.0 で導入されました。

  1. JSON 用の JSONEncoder / JSONDecoder
  2. property list 用の PropertyListEncoder / PropertyListDecoder
  3. NSKeyedArchiver / NSKeyedUnarchiver の拡張と、Foundation の値型への Codable 適合

加えて、エンコーダ・デコーダが共通して使える CocoaError の新しいコードを定めます。

JSONEncoder / JSONDecoder

JSON との往復は JSONEncoderJSONDecoder を通じて行います。トップレベルのAPIは、DataEncodable / Decodable 値の相互変換です。

var encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601

// MyValue は Codable に適合
let value = MyValue(...)
let data: Data = try encoder.encode(value)

var decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decoded = try decoder.decode(MyValue.self, from: data)

フォーマットごとの選択肢は、いくつかの「戦略」プロパティで切り替えられます。デフォルトを使えばそのまま動きますが、API 仕様に合わせて細かく調整できます。

  • outputFormatting: 出力を詰めるか (.compact)、人が読みやすくインデントするか (.prettyPrinted)
  • dateEncodingStrategy / dateDecodingStrategy: Date の表現を選ぶ。Date 自身の既定に任せる(.deferredToDate)、UNIX 秒 / ミリ秒、ISO 8601 文字列(RFC 3339)、任意の DateFormatter、あるいは自分で書いたクロージャ
  • dataEncodingStrategy / dataDecodingStrategy: Data を Base64 文字列として扱う(既定)か、自前のクロージャで符号化する
  • nonConformingFloatEncodingStrategy / nonConformingFloatDecodingStrategy: JSON が直接表現できない Double / Float の特殊値(正負の無限大と NaN)に対する扱い。既定は例外を投げる (.throw)。特定の文字列表現と相互変換したい場合は .convertToString(positiveInfinity:negativeInfinity:nan:) / .convertFromString(...) を指定

JSON に載せられない値を書き出そうとしたり、戦略がエラーを選んでいるときに非準拠な値を踏んだりすると、後述の CocoaError.coderInvalidValue が投げられます。

なお、JSONEncoder / JSONDecoder 自体は Encoder / Decoder プロトコルには適合しません。これらはトップレベルのAPIを提供するクラスで、値の encode(to:) / init(from:) に渡されるのは、内部に隠された実装型です。

PropertyListEncoder / PropertyListDecoder

property list 用のエンコーダ・デコーダも同様に用意されます。戦略の種類は JSON より少なく、主な設定は出力フォーマット(バイナリか XML か)です。

let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let data: Data = try encoder.encode(value)

let decoder = PropertyListDecoder()
let decoded = try decoder.decode(MyValue.self, from: data)

デコード時に、入力がバイナリと XML のどちらだったかを検出したい場合は、format: パラメータ付きのオーバーロードを使うと、実際に読み取ったフォーマットを inout 引数として受け取れます。

共通のエラー語彙

新しいエンコーダ・デコーダで起こりうる典型的な失敗は、CocoaError.Code に共通のコードとして定義されます。これらを使うことで、エンコーダ・デコーダの実装に依存しないエラーハンドリングが書けます。

  • .coderInvalidValue: 出力フォーマットと互換性のない値を符号化しようとした(例: JSON に NaN をそのまま入れようとした)
  • .coderTypeMismatch: 期待した型と実際のデータの型が食い違っていた
  • .coderReadCorrupt: 入力データが壊れているか、フォーマットとして不正だった(既存のコード)
  • .coderValueNotFound: 要求したキーや値が見つからない、あるいは null だった(既存のコード)

これらのエラーは、失敗箇所までの「coding path」を userInfoNSCodingPathErrorKey として、開発者向けの補足説明を NSDebugDescriptionErrorKey として保持します。どのキー経路で壊れたかが分かるため、ネストの深い構造でも原因を追いやすくなります。

NSKeyedArchiver / NSKeyedUnarchiver との相互運用

既存の NSCoding ベースのアーカイブに Codable 値を混ぜて入れられるよう、NSKeyedArchiver / NSKeyedUnarchiverCodable 用のメソッドが追加されます。

extension NSKeyedArchiver {
    public func encodeCodable(_ codable: Encodable?, forKey key: String)
}

extension NSKeyedUnarchiver {
    public func decodeCodable<T: Decodable>(_ type: T.Type, forKey key: String) -> T?
}

加えて、既存の encode(_: Any?, forKey:)Codable 値を受け付けるように改修されます。これにより、NSCoding に適合した既存オブジェクトと、新しく Codable にした値とを、同じアーカイブに交ぜて書き出せます。

アーカイブの意味論として押さえておくべき点は次のとおりです。

  • Foundation の値型(後述)は、アーカイブ時に常に対応する Objective-C 側の型にブリッジされた形で書き出されます。これにより、Objective-C と Swift の双方から同じデータを読めます。読み出し側は、decodeObject(forKey:) で呼べば Objective-C オブジェクトとして、decodeCodable(_:forKey:) で呼べば Swift 値として取り出せます。
  • ユーザー定義の Codable 型はブリッジの対象ではなく、$class 情報を持たないため、書き出したアーカイブは Swift からしか復号できません。

Foundation 値型の Codable 適合

上記に関連して、Foundation の多くの Swift 値型が Codable に適合します。対象は AffineTransform / Calendar / CharacterSet / Date / DateComponents / DateInterval / Decimal / IndexPath / IndexSet / Locale / Measurement / Notification / PersonNameComponents / TimeZone / URL / URLComponents / URLRequest / UUID です。

また、SE-0143 の conditional conformance を使って、Array / Dictionary / Set も要素や値が Codable であるとき Codable に適合します。これらを NSKeyedArchiver 経由で書き出すと、それぞれ NSArray / NSDictionary / NSSet として格納されます。

まとめ

SE-0166 が定めた Codable の骨格に対し、SE-0167 は実用的なエンコーダ・デコーダ(JSON と property list)、共通のエラー語彙、そして NSCoding との橋渡しを一式揃えます。これにより、Swift 4.0 の時点で、新規に書く型を Codable に適合させるだけで JSON や property list と往復でき、既存の NSKeyedArchiver ベースのアーカイブへも段階的に Codable 型を導入していけるようになりました。