0

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?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • You can't. Any is not Decodable. Perhaps it would be better if you explain the original problem you thought you were going to solve? If you have a JSON value that can be a String-Or-Int there is a standard easy way of grappling with that. – matt Jan 23 '20 at 20:24
  • Updated the code. I changed it to `Decodable.Type` but same thing. And it's not just int or string. I also have a 'Foo' in there. Point being it should be a lookup that can handle *any* decodable type. It's just those types are only known/determined at runtime by a caller. The other thing is the dictionary will be passed into the 'userData' of the decoder. It's just where it is here for illustrative purposes. – Mark A. Donohoe Jan 23 '20 at 20:26
  • I am not at all put off by the Int-or-String-or-Foo variant; as I say, there's a standard easy way to do that. It would still be nice to see real code and the real problem. – matt Jan 23 '20 at 20:31
  • @LeoDabus, maybe something got lost in posting that (and where'd all those semis come from!) but this doesn't seem to use 'typeKey', nor the entire lookup for that matter, unless I'm missing something. – Mark A. Donohoe Jan 23 '20 at 20:46
  • What would the JSON here look like? And what would the caller be able to do with the result? If you're just going to `as?` cast it, then there are better solutions to that for almost all real problems. (What you're trying to do is not possible, so everyone's questions are going to be about what your deeper goal is.) There's already a tool that can decode any known decodable type. It's called JSONDecoder. Nothing can decode an unknown (and therefore ambiguous) type. – Rob Napier Jan 23 '20 at 20:47
  • @MarkA.Donohoe Is the list of possible types bounded, as shown in your code above? (e.g. you know that you only ever expect to decode `Int` or `String` or `Foo`?) Or are you looking for totally arbitrary registration capabilities? – Itai Ferber Jan 23 '20 at 20:48
  • I'll add more code and comments to clarify it. Gimme a sec... – Mark A. Donohoe Jan 23 '20 at 20:49
  • 1
    @MarkA.Donohoe The dead-simple change to your code above (if it actually represents your use-case) is turning the array lookup into a `switch`, and manually calling `decode` with the concrete type internally – Itai Ferber Jan 23 '20 at 20:50
  • Why don't you try checking to decode one type after other? – Leo Dabus Jan 23 '20 at 20:50
  • The way to know whether your code can work is whether you can assign a type to `concreteObject`. I mean type `let concreteObject: WhatGoesHere = ...`. "WhatGoesHere" has to be known at compile-time. The fact that the compiler will do type-inference so you don't have to say it explicitly doesn't mean it doesn't have to be *known* explicitly. Otherwise, when you then said `let x: Int = concreteObject`, should that compile or not? – Rob Napier Jan 23 '20 at 20:52
  • @ItaiFerber, that's exactly what I'm doing in my latest 'iteration'. Instead of passing in a lookup of key-to-types, I'm now passing in a closure that takes the typeKey, the container key for the value, and the container itself. Then as you said, I can hard-code in the known types and decode them right there. I was just wondering if I was taking three lefts to go right. In languages like C#, you can easily pass in a type at runtime, even creating generic instances from types in variables. Seems Swift isn't quite so forgiving. But the closure approach seems to be the most viable. – Mark A. Donohoe Jan 23 '20 at 20:59
  • Put that as an answer and I'll vote it as accepted. – Mark A. Donohoe Jan 23 '20 at 20:59

2 Answers2

2

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 (e.g. { "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 (e.g. 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).

Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • There's also a lot more detail (that I don't have room to represent in the answer) in the Swift forum thread [Serializing a dictionary with any codable values](https://forums.swift.org/t/serializing-a-dictionary-with-any-codable-values/16676/1) – Itai Ferber Jan 23 '20 at 21:35
  • Thank you `self = .string(try container.decode(String.self))` was the correct solution to the error. APpreciate it! – Mark Perkins Jan 14 '21 at 23:58
0

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..

gadu
  • 1,816
  • 1
  • 17
  • 31