SwiftyNote

主にSwiftな技術ブログ

【Swift3】enumでJSONデコーダを作ってみる

JSONデコーダenumで作成したらどうなるかふと思ったので作ってみることに。 最少限度の実装でまずはできるか試して、今度検証したりするとき用のメモ程度に。

個人的にはindirectJSONの階層構造を表現することでちょっとだけ高速になるかもしれないと考えた。 JSONではjson["user"]["name"]のように連想配列的に値にアクセスできる。また、このように同じ階層の値がさらに子を持つことがよくある。連想配列ではハッシュにより任意のキーの値へのアクセスのオーダーはO(1)である、しかし実装によっては(※1)階層が増えれば増えるほどキーアクセスはO(n)となる。なので階層が深いキーなどは通過時に一度パスを保持しておくことでアクセス数が減るのではないかと思ったが、多分、数MB以上のJSONなどでない限り効果が薄すぎてあまり意味ないかもしれない。 とりあえず、まず実用的かは考えずに実装してみる。(indirectを使うとは…言っていない)

// `T`型に変換または`invalidPropertyType`をthrow
func castOrFail<T>(_ any: Any) throws -> T {
    guard let result = any as? T else {
        throw JSONParseError.invalidPropertyType
    }
    return result
}

protocol Decodable {
    static func decode(_ json: JSON) throws -> Self
}

// JSONの値は`String`に変換可能
extension String: Decodable {
    static func decode(_ json: JSON) throws -> String {
        return try castOrFail(json)
    }
}

// JSONの値は`Int`に変換可能
extension Int: Decodable {
    static func decode(_ json: JSON) throws -> Int {
        return try castOrFail(json)
    }
}

// 適当なエラーを定義
enum JSONParseError: Error {
    case invalidJSON
    case invalidPropertyType
}

enum JSON {
    typealias RawValue = Any?

    case json([String: Any])
    
    init?(rawValue: RawValue) {
        guard let json = rawValue as? [String: Any] else { return nil }
        self = .json(json)
    }

    func decode<T: Decodable>(path: String) throws -> T {
        if case .json(let json) = self {
            for node in json {
                if path == node.key { return try castOrFail(node.value) }
                if let newJSON = JSON(rawValue: node.value) {
                    let _: T = try newJSON.decode(path: path)
                }
            }
        }
        throw JSONParseError.invalidJSON
    }

}

// カスタムオペレータの定義
infix operator <| : MultiplicationPrecedence

func <| <T: Decodable>(rhs: JSON, lhs: String) throws -> T {
    return try rhs.decode(path: lhs)
}

// モデルを定義
struct User: Decodable {
    let name: String
    let age: Int
    static func decode(_ j: JSON) throws -> User {
        return try User(
            name: j <| "name",
            age: j <| "age")
    }
}

// JSONデータを用意
let json: Any = [
    "name": "User500",
    "age": 18
]

// デコード
if let json = JSON(rawValue: json),
    let user = try? User.decode(json) {
        print(user.name) // -> User500
        print(user.age) // -> 18
}

とりあえずAny型のJSONデータをデコードできるenumが完成した。あとはネストを考慮したり再起部分を繰り返し処理に変えればもうちょっとましになるかもしれない。 さらに冒頭で考えてた実装をまだ実現できていないので、まだまだ改良の余地がありそう。(それがやりたいなら別にenum使わなくても良かった…)

※1…内部的にキーを連結したものからハッシュが計算可能であればこの限りではない