How to decode an array of values whose types depend on a tag?

余生长醉 提交于 2020-08-22 05:17:08

问题


I have a JSON with an array of values:

[
    { "tag": "Foo", … },
    { "tag": "Bar", … },
    { "tag": "Baz", … },
]

I want to decode this array into an array of structs where the particular type depends on the tag:

protocol SomeCommonType {}

struct Foo: Decodable, SomeCommonType { … }
struct Bar: Decodable, SomeCommonType { … }
struct Baz: Decodable, SomeCommonType { … }

let values = try JSONDecoder().decode([SomeCommonType].self, from: …)

How do I do that? At the moment I have this slightly ugly wrapper:

struct DecodingWrapper: Decodable {

    let value: SomeCommonType

    public init(from decoder: Decoder) throws {
        let c = try decoder.singleValueContainer()
        if let decoded = try? c.decode(Foo.self) {
            value = decoded
        } else if let decoded = try? c.decode(Bar.self) {
            value = decoded
        } else if let decoded = try? c.decode(Baz.self) {
            value = decoded
        } else {
            throw …
        }
    }
}

And then:

let wrapped = try JSONDecoder().decode([DecodingWrapper].self, from: …)
let values = wrapped.map { $0.value }

Is there a better way?


回答1:


Your array contains heterogeneous objects of finite, enumerable varieties; sounds like a perfect use case for Swift enums. It's not suitable for polymorphism, because these "things" are not necessarily of the same kind, conceptually speaking. They just happen to be tagged.

Look at it this way: you have an array of things that all have tags, some are of this kind, others are of a completely different kind, and still others ... and sometimes you don't even recognize the tag. A Swift enum is the perfect vehicle to capture this idea.

So you have a bunch of structs that shares a tag property but otherwise completely different from each other:

struct Foo: Decodable {
    let tag: String
    let fooValue: Int
}

struct Bar: Decodable {
    let tag: String
    let barValue: Int
}

struct Baz: Decodable {
    let tag: String
    let bazValue: Int
}

And your array could contain any instance of the above types, or of an unknown type. So you have the enum TagggedThing (or a better name).

enum TagggedThing {
    case foo(Foo)
    case bar(Bar)
    case baz(Baz)
    case unknown
}

Your array, in Swift terms, is of type [TagggedThing]. So you conform the TagggedThing type to Decodable like this:

extension TagggedThing: Decodable {
    private enum CodingKeys: String, CodingKey {
        case tag
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let tag = try container.decode(String.self, forKey: .tag)

        let singleValueContainer = try decoder.singleValueContainer()
        switch tag {
        case "foo":
            // if it's not a Foo, throw and blame the server guy
            self = .foo(try singleValueContainer.decode(Foo.self))
        case "bar":
            self = .bar(try singleValueContainer.decode(Bar.self))
        case "baz":
            self = .baz(try singleValueContainer.decode(Baz.self))
        default:
            // this tag is unknown, or known but we don't care
            self = .unknown
        }
    }
}

Now you can decode the following JSON:

let json: Data! = """
[
    {"tag": "foo", "fooValue": 1},
    {"tag": "bar", "barValue": 2},
    {"tag": "baz", "bazValue": 3}
]
""".data(using: .utf8)

like this:

let taggedThings = try? JSONDecoder().decode([TagggedThing].self, from: json)



回答2:


may be enum can make your code a bit cleaner. Each case will correspond to the type (tag) of your json. Depending on case you will parse your json to appropriate Model. Anyway there is should be some kind of evaluation which model to choose. So I've came to this

protocol SomeCommonType {}
protocol DecodableCustomType: Decodable, SomeCommonType {}

struct Foo: DecodableCustomType {}
struct Bar: DecodableCustomType {}
struct Baz: DecodableCustomType {}

enum ModelType: String {
  case foo
  case bar
  case baz

  var type: DecodableCustomType.Type {
    switch self {
    case .foo: return Foo.self
    case .bar: return Bar.self
    case .baz: return Baz.self
    }
  }
}

func decoder(json: JSON) {
  let type = json["type"].stringValue
  guard let modelType = ModelType(rawValue: type) else { return }

  // here you can use modelType.type
}



回答3:


You can also use a Dictionary for the mapping:

protocol SomeCommonType {}

struct Foo: Decodable, SomeCommonType { }
struct Bar: Decodable, SomeCommonType { }
struct Baz: Decodable, SomeCommonType { }

let j: [[String:String]] = [
    ["tag": "Foo"],
    ["tag": "Bar"],
    ["tag": "Baz"],
    ["tag": "Undefined type"],
    ["missing": "tag"]
]

let mapping: [String: SomeCommonType.Type] = [
    "Foo": Foo.self,
    "Bar": Bar.self,
    "Baz": Baz.self
]

print(j.map { $0["tag"].flatMap { mapping[$0] } })
// [Optional(Foo), Optional(Bar), Optional(Baz), nil, nil]

print(j.flatMap { $0["tag"].flatMap { mapping[$0] } })
// [Foo, Bar, Baz]


来源:https://stackoverflow.com/questions/46319139/how-to-decode-an-array-of-values-whose-types-depend-on-a-tag

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!