LoginSignup
2
4

More than 5 years have passed since last update.

Codableのデコード時にオブジェクトの型を判定してデコードする

Posted at

やりたいこと

  • Codableを使って、微妙にkey:valueの異なるjsonオブジェクトをデコードしたい
  • 任意のkeyのvalueによってデコードする型を判別し、デコードしたい

具体的には?

例えば以下のようなjsonがあったとして、

  • properties配下のkey:valueが統一されていない
  • properties配下をtypeによって型を判定してデコードしたい
articles.json
[
  {
    "id": "xxxxx",
    "name": "today's News",
    "properties": {
      "type": "News",
      "id": "xxxxxx",
      "name": "news title here",
      "backgroundColor": {
        "alpha": 0,
        "red": 0,
        "green": 0,
        "blue": 0
      }
    }
  },
  {
    "id": "xxxxx",
    "name": "weekly Gossip",
    "properties": {
      "type": "Gossip",
      "id": "xxxxxx",
      "name": "gossip title here",
      "radius": 3.6
    }
  },
  {
    "id": "xxxxx",
    "name": "Technology topic",
    "properties": {
      "type": "Tech",
      "id": "xxxxxx",
      "name": "tech title here",
      "refUrl": "https://xxx"
    }
  }
]

どうパースしたらいいだろう?

結果から書くとこんな感じにしてみた。

dynamicClassDecode.playground
import Foundation
import UIKit

typealias Articles = [Article]

struct Article: Codable {
    let id, name: String
    let properties: Properties
}

extension Article {

    private enum CodingKeys: CodingKey {
        case id, name, properties
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        id = try container.decode(String.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        // decode temporarily to switch type and decode to each detailed classes
        let properties = try BaseProps.init(from: container.superDecoder(forKey: .properties))
        self.properties = try properties.type.metatype.init(from: container.superDecoder(forKey: .properties))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try properties.encode(to: container.superEncoder(forKey: .properties))
    }
}

protocol Properties: Codable {
    var type: PropertyType { get }
    var id: String { get }
    var name: String { get }
}

struct Color: Codable {
    var red : CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0

    // retrieve UIColor from `Color`
    var uiColor : UIColor {
        return UIColor(red: red, green: green, blue: blue, alpha: alpha)
    }

    // assign red, green, blue, alpha from UIColor. name leave it nil
    init(uiColor : UIColor) {
        uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
    }
}

class BaseProps: Properties {
    // this class is prepared just for dynamic switching on decode
    var type: PropertyType
    var id, name: String
}

class NewsProps: Properties {

    // properties in common
    var type: PropertyType
    var id, name: String

    // properties only for this class
    var backgroundColor: Color
}

class GossipProps: Properties {
    var type: PropertyType
    var id, name: String

    var radius: CGFloat
    var backgroundColor: Color?
}

class TechProps: Properties {
    var type: PropertyType
    var id, name: String

    var refUrl: String
}

enum PropertyType: String, Codable {
    case News, Gossip, Tech

    var metatype: Properties.Type {
        switch self {
        case .News:
            return NewsProps.self
        case .Gossip:
            return GossipProps.self
        case .Tech:
            return TechProps.self
        }
    }
}

let jsonStr = """
[{"id":"xxxxx","name":"today's News","properties":{"type":"News","id":"xxxxxx","name":"news title here","backgroundColor":{"alpha":0,"red":0,"green":0,"blue":0}}},{"id":"xxxxx","name":"weekly Gossip","properties":{"type":"Gossip","id":"xxxxxx","name":"gossip title here","radius":3.6}},{"id":"xxxxx","name":"Technology topic","properties":{"type":"Tech","id":"xxxxxx","name":"tech title here","refUrl":"https://xxx"}}]
"""

let jsonData = jsonStr.data(using: .utf8)
do {
    let articles = try JSONDecoder().decode([Article].self, from:jsonData!)
    for article in articles {
        let mirror = Mirror(reflecting: article.properties)
        print(article.properties.self)
        print(mirror.children.compactMap { ($0.label ?? "", $0.value) })
    }
} catch let e {
    print(e)
}

補足

上記をそのままplaygroundに突っ込めば動くと思うので試してみてください。

どうやってtypeでデコードを分岐させてるか

let properties = try BaseProps.init(from: container.superDecoder(forKey: .properties))
self.properties = try properties.type.metatype.init(from: container.superDecoder(forKey: .properties))

ここ↑で、一旦protocolに準拠しただけのBasePropsにデコードしておいて、properties.type.metatypeで型を値として取得し、init(from:)でデコードします。

このやり方については、swift公式docのMetatype Typeの項に記載されてますが、元々はこちらの記事をみつけて、踏襲してるだけです:innocent:
Encoding And Decoding Polymorphic Objects In Swift

typeはenum

で定義しているので、例えばこうやってマッピングできる

case News = "news"
case Gossip = "gossip_news"
case Tech = "tech_news"

BasePropsを継承しない理由

BasePropsは、どのクラスにも共通して存在するので、NewsProps等の具体的なクラスはBasePropsを継承すればいいじゃない、と思うかもしれませんが、そうすると、子クラス固有のプロパティが自動ではデコードされないので、いちいちCodingKeysとencode、initメソッドを用意しないといけないのでコードが冗長になってしまいます(多分2.5倍くらいのコード量)。なので、どのtypeもPropertiesを準拠する形で、継承関係を持たないようにしています。

今回、Mirrorで当該Codableのプロパティ一覧を取得したいという理由もあって、継承関係にすると面倒なので、Protocolに準拠する形で都合が良かったです。

いい点

  • CodingKeysとかinit, encodeメソッドを用意するよりコード量は半分以下に抑えられると思う
  • protocolでメソッドを宣言して、その実装を各具体クラスに任せることができる(ポリモーフィズムれる)

注意点

今回は、判定したい型の種類が多くなる予定だったし、型によって個別の処理をしたいことがあったので、デコードする際に型を分けましたが、そもそもそこまでする必要無い場合もあると思います。そういうときは、共通しないkeyをoptionalにすればいいだけなのでそのほうが楽だと思います。

例えばこういう感じ:

Articles.swift
// To parse the JSON, add this file to your project and do:
//
//   let articles = try? newJSONDecoder().decode(Articles.self, from: jsonData)

import Foundation

typealias Articles = [Article]

struct Article: Codable {
    let id, name: String
    let properties: Properties
}

struct Properties: Codable {
    let type, name: String
    let backgroundColor: BackgroundColor?
    let image: String?
    let radius: CGFloat?
}

struct BackgroundColor: Codable {
    let alpha, red, green, blue: CGFloat
}

これは、ほぼquicktypeで作ったものそのままなので、細かい事は置いといて、見てほしいのは、struct Propertiesで、backgroundColorimageradiusがoptionalになっているところ。

単純ですがこうすれば、型によってあったりなかったりするkey/valueも、optionalなので、Codableがよしなに処理してくれます。

ただ、後段で分岐が多くなったり、optionalをアンラップしないといけないのでコードが冗長になりやすかったりするので、その辺とのトレードオフかと思います。

他にも良いやり方とか、こうするといいよというのがあれば、やさしくコメントかedit request等もらえると嬉しいです。

参考

ほぼこの方のやり方を踏襲してます:relaxed:
https://www.digitalflapjack.com/blog/2018/5/29/encoding-and-decoding-polymorphic-objects-in-swift

wwdc2017 What's New in Foundation video

official swift docs "Encoding and Decoding Custom Types"

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

2
4
0

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
2
4