SwiftのJSONEncoder/DecoderとCodable protocol

(2020-05-26)

SwiftのJSONEncoder/Decoderは JSON文字列をCodable(Encodable & Decodable) protocolを実装したClassやStructにエンコード/デコードするオブジェクト。

全ての変数がCodableで、特に何もする必要がない場合はCodableを付ければうまくいく。 String、Int、Doubleといったstandard libraryの型や、DateやDataなどFoundationの型はCodableになっている。

CodingKeyでフィールド名のマッピングができる。デコード時にOptionalでないフィールドが足りなかったり型が異なると例外が飛び、エンコード時にはnilを無視する。

struct Foo: Codable {
    var nums: [Int]
    var str: String
    
    enum CodingKeys: String, CodingKey {
        case nums = "numbers"
        case str
    }
}


var data = """
{
    "numbers": [100, 120],
    "str": "Aaa"
}
""".data(using: .utf8)!
let json = try! JSONDecoder().decode(Foo.self, from: data)
print(json.str) // => Aaa
let enc = try! JSONEncoder().encode(json)
print(String(data: enc, encoding: .utf8)!) // => {"numbers":[100,120],"str":"Aaa"}

var data = """
{
    "numbers": [100, 120],
}
""".data(using: .utf8)!
let json = try! JSONDecoder().decode(Foo.self, from: data) // Swift.DecodingError.keyNotFound

Codableの関数を自分で実装すれば柔軟に処理できる。次の例ではJSONのbar.strをFoo.strに対応させている。

struct Foo {
    var nums: [Int]
    var str: String

    enum CodingKeys: String, CodingKey {
        case nums = "numbers"
        case bar
        case str
    }
    
    enum BarKeys: String, CodingKey {
        case str
    }
}

extension Foo: Codable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        nums = try values.decode([Int].self, forKey: .nums)
        
        let barValues = try values.nestedContainer(keyedBy: BarKeys.self, forKey: .bar)
        let barstr = try barValues.decode(String.self, forKey: .str)
        str = barstr
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(nums, forKey: .nums)
        
        var bar = container.nestedContainer(keyedBy: BarKeys.self, forKey: .bar)
        try bar.encode(str, forKey: .str)
    }
}



var data = """
{
    "numbers": [100, 120],
    "bar": {
        "str": "aiueo"
    }
}
""".data(using: .utf8)!
let json = try! JSONDecoder().decode(Foo.self, from: data)
print(json.str) // => aiueo
let enc = try! JSONEncoder().encode(json)
print(String(data: enc, encoding: .utf8)!) // => {"numbers":[100,120],"bar":{"str":"aiueo"}}