はじめに
Swift 4対応の一つ、Codableについていくつか疑問点を実験しつつ、日本語でまとめました。
元記事は以下ですが、たぶん元記事よりも補足しています。
Encoding and Decoding Custom Types
4/9追記: Swift4.1(というかXcode 9.3)で追加されたkeyEncodingStrategy, keyDecodingStrategyと、それに合わせてCodingKeyプロトコルについて追記しました。
おまけ: PlaygroudとかでJSON表示するだけのコード何回も書いてたのでgistにおきました。良かったらご利用ください。
PrintEncodable.swift
Codable, Encodable, Decodable
typealias Codable = Decodable & Encodable
DecodableとEncodableどっちもにconformしたのがCodable
クラスにも構造体にも適用可能です。
自動Codable
String, Int, Double, Date, Data, URLは既にCodable
Array, Dictionary, Optional も中身がCodableであればCodable
Codableなプロパティのみから構成された構造体はCodable
よって、以下ののように書くだけでCodableとして扱える
struct Landmark: Codable {
var name: String
var foundingYear: Int
}
CodingKeys
エンコード/デコードのキーが一致しない時に用いる。補完とかされないけれど、構造体内部にこの名前で定義すると使える。
case名をプロパティ名、rawValueをエンコード結果のフィールド名として定義する
case自体を省略するとエンコード・デコードされない。この時、Decodableにするにはdefault valueが必要。
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)
詳しくはJSONEncoerの節で紹介します。
Manual Implementation
データがネストしているときなど、エンコードのフォーマットがデータ構造と一致しない時は、自分で encode()
メソッドを実装する必要がある。
フィールドの定義は同じようにCodingKeyを使う
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
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等を使う。
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にするとキー順でソートされる
.prettyPrintedにすると人間の読みやすいJSON形式になる
encoder.outputFormatting = .prettyPrinted
{
"additionalInfo" : {
"elevation" : 0
},
"longitude" : 0,
"latitude" : 0
}
複数まとめて設定したりもできる
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
但し、あくまでルート以下のkeyに対する設定で、ネストされたCodingKeyはそれぞれの方式に従います。
useDefaultKeysは読んで字のごとくそのままです。
convertToSnakeCase
最も一般的であろう、「SwiftでcamelCase、JSONでsnake_case」を用いる場合は、以下のようにします。
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
なクロージャで指定する。
ドキュメントのサンプルで紹介されていた例は、以下の様なエンコード。
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プロトコルの節でもう少し書きます)
/// 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
}
}
そしてエンコードは以下のように定義すると、さっきの出力が得られる。
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は必須っぽい。
(出典: The JSON Data Interchange Syntax)
EncoderはJSONだけではないのでintValue
が用意されているんだと思うが、具体的な用途は不明。
継承クラスのエンコード
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"}
継承元クラスのプロパティはエンコードされません。
enum CodingKeys: String, CodingKey {
case name
case dogName
}
もコンパイルエラーになります。
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にすると、今度は親クラスのプロパティしかエンコードされません
よって、このような場合は以下のように手動で定義する必要があります。
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のエンコード
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はエンコードされません。
enum CodingKeys: String, CodingKey {
case name
case name2
}
は note: CodingKey case 'name2' does not match any stored properties
としてコンパイルエラーになります。
よってこの場合も手動で実装する必要があります。
EncodingContainer
自分でencodingを実装する場合に使うものです。
型 | 役割 |
---|---|
KeyedEncodingContainer | 辞書 |
UnkeyedEncodingContainer | アレイ |
SingleValueEncodingContainer | 単一値 |
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のエンコード結果が単一値であるとエラーになる(一番外側はアレイもしくは辞書でなければいけない)ので、注意が必要です。
extension RealmOptional: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
ネストさせる場合(辞書やアレイの場合)は、
// 辞書
container.nestedContainer
// アレイ
container.nestedUnkeyedContainer
などでcontainerを取得できます。
Codableなenum
rawvalueが入ります
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 に準拠している場合は、標準実装で、RawValueに対してエンコード/デコードされます。