简体   繁体   中英

How do you decode a known, concrete type if that concrete type is stored in a variable of 'Decodable.Type' at runtime?

Little stumped by something that's best illustrated with a class...

class AnyDecodableWrapper : Decodable {

    static let decodableTypesLookup:[String:Decodable.Type] = [ <-- 'Decodable.Type' here is what's causing the problem
        "str": String.self,
        "int": Int.self,
        "foo": Foo.self
    ]

    enum CodingKeys : String, CodingKey {
        case typeKey
        case value
    }

    required init(from decoder: Decoder) throws {

        // Get the container for the CodingKeys
        let container = try decoder.container(keyedBy: CodingKeys.self)

        // Get the key to look up the concrete type
        typeKey = try container.decode(String.self, forKey:.typeKey)

        // Attempt to get the concrete type from the key
        guard let concreteType = AnyDecodableWrapper.decodableTypesLookup[typeKey] else {
            value = nil
            return
        }

        // Attempt to decode an instance of the concrete type
        let concreteObject = try container.decode(concreteType, forKey: .value)

        value = concreteObject
    }

    let typeKey : String
    let value   : Any?
}

The problem is the line assigning the temp concreteObject complains with the following...

Ambiguous reference to member 'decode(_:forKey:)'

This is of course because the type returned from the dictionary is Decodable.Type and not something like 'String.self' thus it's not sure which decode overload to use.

So if you have the concrete type stored in a variable of Any.Type , how can you pass that to the correct decode overload?

KeyedDecodingContainer.decode(_:forKey:) (and the other containers' decode(...) methods) is a generic method, taking the type parameter as a generic input. In order to call the method, then, Swift needs to know the generic type statically, at runtime. Although you have a value of Decodable.Type , in order to dispatch the method call, Swift would need the specific type at compile-time.

As mentioned in a comment, the simplest change you can make to the above code is to lower your generalization down into specific decode<T>(_:forKey:) calls:

switch typeKey {
case "int": value = try container.decode(Int.self,    forKey: .value)
case "str": value = try container.decode(String.self, forKey: .value)
case "foo": value = try container.decode(Foo.self,    forKey: .value)
default:    value = nil
}

This specifies to the compiler the generic type to dispatch the call. Depending on your specific needs there are other solutions out there, but this is the gist of what you'd eventually need to do. (You can indeed wrap the calls in closures to add a layer of indirection.)


This is assuming that you have to match JSON which specifies the type inline (eg { "type": "int", "value": 42 } ), but if you have control over the JSON data, you can likely avoid this by creating, say, an enum that represents all possible types you expect (as laid out in How to deal with completely dynamic JSON responses ):

enum IntOrStringOrFoo: Decodable {
    case int(Int)
    case string(String)
    case foo(Foo)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = .int(try container.decode(Int.self))
        } catch DecodingError.typeMismatch {
            do {
                self = .string(try container.decode(String.self))
            } catch DecodingError.typeMismatch {
                self = .foo(try container.decode(Foo.self))
            }
        }
    }
}

Instead of looking for a type selector in the decoded data, you can attempt to decode as the types you need, and stick with what succeeds. This assumes that types don't overlap (eg might be decodable as one another, like Int8 and Int , in which case you'd need to be very careful about the order you decode in).

You could always use a giant switch statement checking every possible value (ew) but if you were going to do that the much better way would be to use type erasure.. the downside is you pretty much have to go through all your possible classes and declare them as conforming to, or implement a func that returns the decodable type - which can be cumbersome.

Ideally we could just get "out of the box" support for anything that's Decodable .

To do this we would require a language feature called opening existentials - there's a great and detailed write up of the issues you're facing in this question ( Protocol doesn't conform to itself? )

Anyways, no clue if this works but I WAS able to get something to compile that seems to do what you want:

class AnyDecodableWrapper : Decodable {

    static let decodableTypesLookup: [String: Decodable] = [
        "str": "",
        "int": 0
    ]

    enum CodingKeys : String, CodingKey {
        case typeKey
        case value
    }

    private struct DecodableObject: Decodable {}

    required init(from decoder: Decoder) throws {

        // Get the container for the CodingKeys
        let container = try decoder.container(keyedBy: CodingKeys.self)

        typeKey = try container.decode(String.self, forKey:.typeKey)

        guard let concreteType = AnyDecodableWrapper.decodableTypesLookup[typeKey] else {
            value = nil
            return
        }

        let concreteObject: DecodableObject = try container.unambiguousDecode(concreteType, forKey: .value)
        value = concreteObject
    }

    let typeKey : String
    let value   : Any?
}

extension KeyedDecodingContainer {
    func unambiguousDecode<T: Decodable>(_ decodableType: Decodable, forKey key: KeyedDecodingContainer<K>.Key) throws -> T {
        let decodable = type(of: decodableType) as! T.Type
        return try decode(decodable, forKey: key)
    }
}

Explanation:

The issue is that the compiler can't figure out which of the 16 decode: methods to use on KeyedDecodingContainer .

Because swift lacks opening existentials it can't tell that passing an argument of Decodable.Type is the same as T.Type where T: Decodable

So the first thing I did was work with Decodable directly instead of Decodable.Type . That meant changing both the decodableTypesLookup to take be [String: Decodable] and creating my own decoding method on KeyedDecodingContainer that took in a Decodable instead of a meta type (like the standard decode: func takes).

The line that does worry me here is:

let decodable = type(of: decodableType) as! T.Type

If that doesn't way perhaps there's a way to cast to a generic type instead of using as! ?

Anyway, that left me with an error of (Generic parameter T could not be inferred) on

let concreteObject = try container.unambiguousDecode(concreteType, forKey: .value)

and that's just because you can't have an actual concrete Decodable() and it has no idea what the concrete object should be.

To get around that I just created a throw-away:

private struct DecodableObject: Decodable {}

Just so I can tell the compiler that should be the concrete object:

let concreteObject: DecodableObject = try container.unambiguousDecode(concreteType, forKey: .value)

we can then obviously covert that to the expected type Any? (perhaps AnyObject would be better tbh)

Hope it works..

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM