11

Is it possible with the Decodable protocol in Swift 4 to decode a JSON object when the type to decode to is only known at runtime?

I have a registry of sorts which maps a String identifier to the type we want to decode to, as below:

import Foundation

struct Person: Decodable {
    let forename: String
    let surname: String
}

struct Company: Decodable {
    let officeCount: Int
    let people: [Person]
}

let registry: [String:Decodable.Type] = [
    "Person": Person.self,
    "Company": Company.self
]

let exampleJSON = """
{
    "forename": "Bob",
    "surname": "Jones"
}
""".data(using: .utf8)!

let t = registry["Person"]!

try! JSONDecoder().decode(t, from: exampleJSON) // doesn't work :-(

Am I on the right lines here or is there a better way?

Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
Dave Rogers
  • 135
  • 7

2 Answers2

14

Your design is indeed unique but, unfortunately, I believe you are hitting an edge case of Swift's type system. Basically, a protocol doesn't conform to itself and, as such, your general Decodable.Type isn't enough here (i.e., you really need a concrete type to satisfy the type system requirements). This might explains the error you are having:

Cannot invoke decode with an argument list of type (Decodable.Type, from: Data). Expected an argument list of type (T.Type, from: Data).

But, having said that, there is indeed a (dirty!) hack around this. First, create a dummy DecodableWrapper to hold your runtime-ish Decodable type:

struct DecodableWrapper: Decodable {
    static var baseType: Decodable.Type!
    var base: Decodable

    init(from decoder: Decoder) throws {
        self.base = try DecodableWrapper.baseType.init(from: decoder)
    }
}

then use it like this:

DecodableWrapper.baseType = registry["Person"]!
let person = try! JSONDecoder().decode(DecodableWrapper.self, from: exampleJSON).base
print("person: \(person)")

prints the expected result:

person: Person(forename: "Bob", surname: "Jones")

Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
  • @LeoDabus the properties come from the type returned by `registry["Person"]!`. My `DecodableWrapper` only *forwards* the decoding to his specific type, without assuming any specific data scheme. – Paulo Mattos Oct 06 '17 at 01:29
  • No, it can't be anything but a `Person`-ish object. His code clearly assumes (i.e., `registry["Person"]`) that a person-like object will be decoded. He just doesn't know (at compile time) *which* type will hold said person. The jury is out on how useful such a dynamic mechanism will help him at all. But it's nice to know that *Swift 4 Coding framework* con work around such exotic design... ;) – Paulo Mattos Oct 06 '17 at 01:33
  • I feel we would need further info -- from good @DaveRogers here -- to fully evaluate if this design will be a good fit for his overall architecture. – Paulo Mattos Oct 06 '17 at 01:44
  • 1
    @LeoDabus ...yet another tool in the hacker's toolbox. Might come handy in rainy day, who knows? ;) – Paulo Mattos Oct 06 '17 at 02:00
  • Thanks Paulo, this is a great step in the right direction! The static does feel a little bit dirty as you say but it'll do for now ;-) – Dave Rogers Oct 06 '17 at 07:26
10

The workaround by Paulo has the disadvantage of it not being thread safe. Here's an example of a simpler solution that will allow you to decode a value without having the concrete type available:

struct DecodingHelper: Decodable {
    private let decoder: Decoder

    init(from decoder: Decoder) throws {
        self.decoder = decoder
    }

    func decode(to type: Decodable.Type) throws -> Decodable {
        let decodable = try type.init(from: decoder)
        return decodable
    }
}

func decodeFrom(_ data: Data, to type: Decodable.Type) throws -> Decodable {
    let decodingHelper = try JSONDecoder().decode(DecodingHelper.self, from: data)
    let decodable = try decodingHelper.decode(to: type)
    return decodable
}
Michael Waterfall
  • 20,497
  • 27
  • 111
  • 168