anz blog

Codable で root 階層無視したい場合

2018-06-14 #Swift

CodableというかDecodableの話か。。。

とある json で root 階層を無視してマッピングするには...?

という話

Swift 4 イチオシ(?)の機能の一つでもあるCodable
使っているときにふと...あーこれどうしよ...ってなったのでそのメモ

環境

  • Xcode 9.4

たとえば...

このような json

let jsonString = """
{
    "status": 200,
    "access_date" : "2018-06-13",
    "user": {
        "id": 1,
        "name": "anz.factory"
    }
}
"""

どう定義していこうか...🤔

純粋にただ行うなら...

2つ用意してそれぞれマッピングしていく感じですかね。

struct Root: Codable {
    private enum CodingKeys: String, CodingKey {
        case status, accessDate = "access_date", user
    }
    let status: Int
    let accessDate: String
    let user: User
}

struct User: Codable {
    var id: Int
    var name: String
}

let root = try JSONDecoder().decode(Root.self, from: jsonString.data(using: .utf8))
print(root.user.name)

これでok!
なのですが... Root って絶対使わない...これを定義する意味...😓

Root なしでしたいよう...

上記のような感じでDecodableに完全にまかせてマッピングさせることもできますが、
そこを自分でコントロールすることもできます。
そこでnestedContainer(keyedBy:,forKey:)というものを使うといけます!

struct User: Codable {
    // keyの定義だけはする
    private enum RootKeys: String, CodingKey {
        case status, accessDate = "access_date", user
    }
    
    var id: Int
    var name: String
    
    init(from decoder: Decoder) throws {
        // root
        let root = try decoder.container(keyedBy: RootKeys.self)
        // user
        let user = try root.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
        id = try user.decode(Int.self, forKey: .id)
        name = try user.decode(String.self, forKey: .name)
    }
}

// User をつかえる!
let user = try JSONDecoder().decode(User.self, from: jsonString.data(using: .utf8))
print(user.name)

こういう感じ。
これでわざわざ Root を定義しなくてもできます!

ただ...
今回 User のプロパティが少ないからこれでいいけど...
多いとこっちのほうが断然めんどくさくなるので...そこはトレードオフで Root を定義する方を選ぶかもですね(笑)

json のフラット化も同じ感じでいけますよ!

let jsonString = """
{
    "name": "image.png",
    "size": {
        "width": 60,
        "height": 40
    }
}
"""

// size をフラット化したい!

struct ImageData {
    
    private enum CodingKeys: String, CodingKey {
        case name, size
    }
    
    private enum SizeKeys: String, CodingKey {
        case width, height
    }
    
    var name: String
    var width: Int
    var height: Int
}

extension ImageData: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        let size = try values.nestedContainer(keyedBy: SizeKeys.self, forKey: .size)
        width = try size.decode(Int.self, forKey: .width)
        height = try size.decode(Int.self, forKey: .height)
    }
}

こういう感じ。
プロパティと CodingKeys が合わないので、 Decodableだけで...
Codableでやる場合は、encode(to encoder:) throwsもちゃんとやるとできます!👍

参考

Encoding and Decoding Custom Types | Apple Developer Documentation