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 struct
s 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?
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)
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
}
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]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With