LoginSignup
217
178

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-08-28

はじめに

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は必須っぽい。

cca9bd6b0d53a747f2f9a8c63abbd9ca-1.png

(出典: 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に対してエンコード/デコードされます。

217
178
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
217
178