Trouble decoding instances of a protocol


#1

Why doesn't this compile? And is there a good alternative?

I'm trying to decode some instances of a protocol called Marker. I don't know the specific class of these instances ahead of time, so I can't put the class names in my code. Instead, I read a string, which I use look up the specific class that implements Marker, and then I read a Data which is the encoded JSON. So I have the type as a Marker.Type, but apparently you can't pass that to decode.

A boiled down example:

protocol Marker : class, Codable {  }
class Foo : Marker {
    let someProperty = "foo"
}
let enc = JSONEncoder()
let foo1 = Foo()
let data1 = enc.encode(foo1)
let t: Marker.Type = Foo.self
let dec = JSONDecoder()
let foo2 = dec.decode(t, from: data1)
// works: let foo2 = dec.decode(Foo.self, from: data1)

error: Untitled Page.xcplaygroundpage:14:16: error: cannot invoke 'decode' with an argument list of type '(Marker.Type, from: Data)'
let foo2 = dec.decode(t, from: data1)
Untitled Page.xcplaygroundpage:14:16: note: expected an argument list of type '(T.Type, from: Data)'
let foo2 = dec.decode(t, from: data1)


(Morten Bek Ditlevsen) #2

My understanding:
You need a concrete type conforming to Decodable since the compiler would not know which concrete instance of init(from decoder: Decoder) to call otherwise.

A solution could be to create a custom decodable wrapper type that looks at the data and determines which concrete type to use for decoding. The wrapper type could be an enumeration with all cases carrying a concrete type as their associated value. Not at a computer right now, but I could write a small example if you would like.


#3

Maybe there is something fundamental about Swift that I'm missing. If this were Java or C# then decode(...) would be receiving as it's first parameter the "class object" representing the Foo class, and that would give the internals of decode enough reflection info to find the concrete type and the Foo.init that it needs.

For example, I can simply cast the argument: dec.decode(t as! Foo.Type, data1) and it works.

I don't see why the static type of the variable passed to decode(...) should matter. The run time type is correct, so it should be able to do it's job and create an instance of Foo.


(Morten Bek Ditlevsen) #4

If you are decoding a json structure, where would you expect this “class object” to come from?

If you are serializing and deserializing class hierarchies I think that NSCoding and NSSecureCoding is a more appropriate tool. These let you serialize instances with references to other instances. Codable cannot do that (yet).


#5

It's in my example code snippet: Foo.self. In Java it would be Foo.class. Is Foo.self not a "type object" or "class object" - a thing with meta/reflection info about the Foo type?


(Morten Bek Ditlevsen) #6

Ah, now I get the point of your question. :slight_smile:
So the reason that does not work is that the api for decodable uses generics, which I guess you could call a compile time feature and not a runtime feature.
The interface is something like func decode(...) where T: Decodable.
I guess that it would be possible to have modeled the api without using generics, so that the api was decode(_ t: Decodable.Type).
In that case your code would compile.
I am not 100% certain why generics were chosen, but it could be a question about the state of reflection or a performance consideration. I hope that more knowledgeable people can chime in here. :slight_smile:

I know that I am not answering your question entirety, but at least it’s part of the picture. :slight_smile:


(Morten Bek Ditlevsen) #7

Ok, I was pondering about the question about why generics are used, and I think I know the answer.
If the api was dynamic, then it would always just return a value of type Decodable, and afterwards you would need to cast it. Using generics you can statically determine the type of the decoded value.


(Morten Bek Ditlevsen) #8

Ok, here is a really hackish way to go about the decoding!

I mean - like extremely hackish!

Use a wrapper type - and then pass the actual type in somehow - the only way I could think of was using userInfo..

import Foundation

protocol Marker: class, Codable {}

class Foo: Marker  {
    let someProperty = "foo"
}

let typeKey = CodingUserInfoKey(rawValue: "x")!

let enc = JSONEncoder()
let foo1 = Foo()
let data1 = try! enc.encode(foo1)

struct Wrapper: Decodable {
    let m: Marker
    init(from decoder: Decoder) throws {
        let t: Marker.Type = decoder.userInfo[typeKey] as! Marker.Type
        self.m = try t.init(from: decoder)
    }
}


let t: Marker.Type = Foo.self
let dec = JSONDecoder()
dec.userInfo[typeKey] = t

let w = try dec.decode(Wrapper.self, from: data1)
let m = w.m
dump(m)



#9

Common sense suggests that JSONDecoder might be a subtype of Decoder and that this would work:

let dec = JSONDecoder()
let foo = t.init(from: dec)

But apparently not:

error: argument type 'JSONDecoder' does not conform to expected type 'Decoder'

So I may end up doing something like what you just wrote.

I also may file a Swift bug report, because I still think my original code example should work.


(Morten Bek Ditlevsen) #10

Right, in the implementation of JSONDecoder you can see that it is not in itself a Decoder, but instead has an internal _JSONDecoder instance that -is- a Decoder.

I’m pretty certain that your example is not supposed to work by design. Again others are welcome to chime in here! :-)

Could your code not be structured in a way where you also use generics, so that the type information is always present?


#11

The underlying problem here is that, because decode is generic, the compiler needs to deduce the type of the generic parameter T, which must be a concrete type conforming to protocol Codable. Neither Codable nor Marker is a concrete type conforming to Codable,

Passing a parameter which is a non-literal type value, as you were trying to do, would satisfy the runtime requirements of the decode method (it would know what type of value to create), but doesn't satisfy the compile-time requirements, since the compiler can't deduce what T is.

Note that the requirement that T be a concrete type comes from using it in a parameter. If the function omitted that parameter, the compiler could deduce T from the return type (if it knew the type of the expression you are using the result in), but the method isn't set up that way because deducing T from the return type is often going to be impossible, or is going to be an unintended type.


#12

Does "concrete type" mean "not a protocol"?

One cause of my confusion was that I didn't know why the compile time requirement was not satisfied. I didn't know (or forgot) that protocols never "conform" to protocols, including themselves; so the requirement on the generic parameter (T ... where T : Decodable) is not satisfied by protocols like Marker.

If I use a base class for Marker, things work as I'd originally expected. But I'll probably use a type-erased wrapper like Morten suggested.

Rob


#13

"Concrete type" means a type you can create a concrete value of. You can't create a concrete value of a protocol because protocols don't have any storage (and may not have implementations of their own requirements).

I'm not sure offhand if there's anything else that is regarded as a non-concrete type. :slight_smile:

The thing about protocols not conforming to themselves is just one of those ugly details that will jump up and bite you if if don't keep it in mind, and it's not unusual for this to occur when using Codable, since that often involves mucking about with types and type transformations.

A type-erased wrapper is a good way to go. The thing is, AFAIK, a protocol actually has a type associated with it for the compiler to use internally. Ideally, this would really be a type-erased type conforming to the protocol — that would solve all these problems — but it's either too hard to turn this outwards for general consumption, or there is some technical block standing in the way, not sure which.


(Quinn “The Eskimo!”) #14

A type-erased wrapper is a good way to go.

If you go down this path you have to be very careful about security. The way NSCoding avoids this problem is by encoding the class name in the archive and then creating the class by name on the decode. This is Bad™ from a security perspective, so much so that Apple is actively encouraging folks to move to NSSecureCoding (for example, WWDC 2018 Session 222 Data You Can Trust). If you roll your own solution to this in the JSON space, you need to make sure it’s similarly safe.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple