Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 746
zoul Avatar asked Sep 20 '17 10:09

zoul


3 Answers

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)
like image 117
Lord Martinique Avatar answered Sep 20 '22 16:09

Lord Martinique


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
}
like image 43
Mikhail Maslo Avatar answered Sep 21 '22 16:09

Mikhail Maslo


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]
like image 23
BallpointBen Avatar answered Sep 22 '22 16:09

BallpointBen