Protocol as Type question

Hi everyone,

Given this little snippet of a Codable struct:

struct Foo: Codable {
  var id: String
  var userInfo: Dictionary<String, Codable>?
}

which doesn't compile (obviously?!). I assume it is because the compiler cannot really determine the "final" effective Type of the userInfo var (Xmas wish; if only protocols could be treated as Types).

But we (humans) could (obviously!) say "It's a dictionary of Key(s) that are String and Value(s) are Codable" which magically makes the whole struct Codable... or not ...

So what are the current best solutions to this problem?

1- I could generic-ize the struct Foo<T: Codable> and then use T as the Value type of userInfo but this is not very helpful to me since I want to have heterogeneous dictionary Value types (like Double, String, or any other Codable Type) as in:

foo.userInfo = ["name": "Bart", "age": 12, "lastViewDate": Date()]

2- I could have an enum value to encapsulate the type and make the enum Codable (we loose all the magic...):

enum CodableValue: Codable {
  case double(Double), date(Date), string(String) /*,... ad lib... (I'm getting sick)*/

  init(from decoder: Decoder) throws {...}
  func encode(to encoder: Encoder) throws {...}
}

And then define Foo's userInfo as:
var userInfo: Dictionary<String, CodableValue>?

But the drawback is I get to switch for each value to access it and also it's not very swifty!

Any advice is most welcome.

Best regards,
Thierry

I think this is a duplicate of this question

1 Like

Well I don't completely agree with you. But who cares ... :slight_smile:
So ... my question is more "what's the latest Swift way to tackle this"

The problem remains the same. Simplified:

struct Foo: Codable {
    var bar: Codable
}

Encoding an existing Foo instance would work fine here. But how would you decode some incoming data into a Foo instance without knowing the concrete type of bar? You have already discovered the generic solution:

struct Foo<T: Codable>: Codable {
    var bar: T
}

Here, decoding is easy, since you must give a concrete type for T. If, however, you have a heterogenous dictionary that can’t be typed that easily, you have to think about how you are going to decode it. This is something the compiler can’t do for you.

One possible option I have used in the past when working with JSON is to keep a part of the structure “generic”, which is a bit like the second solution you have proposed:

struct Foo: Codable {
    var id: String
    var userInfo: JSON?
}

…where JSON is zoul/generic-json-swift. This sort of sidesteps the issue, keeping the userInfo schema loose.

1 Like

@zoul thanks, that's what I was afraid of...

Dear swift core team,

I've been a good boy, developing in protocol oriented manner,
I've been using generics to get rid of boilerplates,
I have been constantly re-writing many codes from Swift 1 to Swift 2 to Swift 3 to Swift 4,
I've been socialising with other less appealing languages but always went back to Swift ;),
Please, give us a swift compiler that recognises Protocols as real types !

Amen.:sunglasses:

Although I understand your feeling :–), I don’t think that’s the issue here. Let’s get back to the simplified example:

struct Foo: Codable {
    var bar: Codable;
}

Let’s say this compiles. You receive some JSON payload that you want to decode into an instance of Foo. How would the compiler know how to decode the incoming data for bar?

6 Likes

Put in a slightly simpler way, we can demonstrate this by taking Codable out of the question altogether.

The issue at hand is the following:

protocol Foo {
    init()
}

Foo.init() // <- what instance of `Foo` does this create?

This is exactly what is happening inside of any Decoder — it needs to be able to call init(from:) on a type it doesn't know. How could it instantiate a type without knowing what it is? The only way to do this is to pass a concrete type in generically to specify exactly what you intend.

9 Likes

…or bake the types into the encoded data, which is what Objective-C’s NSCoding toolchain does. (That’s maybe the reason people so often expect this to work?) But that brings a whole new set of problems – it’s mostly a completely different and IMHO much less useful design.

Do you have a particular use case, @Orion98mc?

2 Likes

@zoul Right I see your point...Your example is much more constrained than mine, where it was a Dictionary.

struct Foo: Codable {
  var dict: Dictionary<String: Codable>
}

In my example, the type is a Dictionary and there is a constraint on the Value of each Key/Value pairs that says ... "every value of the dictionary has to be Codable"

Now, I understand that the compiler cannot guess if 1 has to be decoded as Int or as Double. But this should be left to the decoder() to take care of making choices much like it does so when decoding Dates. This is why I think I'm constantly fighting against either the framework or the compiler. In my opinion the compiler should be able to compile such statements.

@itaiferber, yes I get your point, in your case it is really obvious that the compiler cannot infer any concrete type for Foo.init() and this is really given. The example of the Dictionary is much more reasonable. besides Encodable / Decodable are mostly meant to be used with coders/decoders which act as the man in the middle in our little scenarios...

Yes, of course — I took it as a given that we're concerned with the design space that Codable inherits; there are of course other ways to go about it, like NSCoding.

I don't necessarily agree with "less useful", but yes, this is a different design, with a different set of tradeoffs, security concerns, etc.

I don't agree that this should be the case either. If you go to decode "2018-12-13T00:00:00Z", do you want the decoder to guess that this is a Date, or are you expecting a String back? What about a custom struct MyDate that you have in your project that you're actually expecting to decode from such a string? [Note that even with JSONDecoder's .dateDecodingStrategy, you still have to ask for a Date for the strategy to take effect — if you ask to decode a String you'll just get back a String.] Similarly, is "https://swift.org" a String, or a URL? (Because if the Decoder tried to pass the String through to URL.init(from:), the URL would fail to decode because it expects to decode from a KeyedDecodingContainer.)

It's not that the compiler can't compile what you're asking due to a limitation, but rather the opposite: we designed Codable intentionally to make use of the fact that you have to specify what you mean in cases like this because it is impossible for the Decoder to guess at your intent. Really, there is absolutely no way of knowing what you mean.

11 Likes

@itaiferber Okay I get why it could be impossible to Decode arbitrary things without any knowledge of them. Makes sens.

What about Encodable ?

struct Foo: Encodable {
  var bar: Dictionary<String, Encodable>
}

Let's says it compiles... (but It does not!)

var foo = Foo(bar: ["Hello": "Swift"])

There shouldn't be any problem to encode that IMHO.

Indeed, the limitation on encoding is artificial (it would be easy to encode an Encodable) — however, it is not without value. This is an intentional choice to help prevent developers from writing code which encodes data, but is then unable to decode it later on. From years of experience, we're trying to prevent the case where v1 of your app is based on a data model written around this capability, and in v2 of your app you realize that all of the user data you've serialized is irretrievable as-is because it's impossible to decode back.

Data loss like this is no joke, so the goal was to make it difficult to do. You can easily work around this by writing an AnyEncodable type yourself, for instance, with the knowledge that you won't be able to decode it again at a later date.

5 Likes

To quote @itaiferber from an older thread on this topic:

This was a conscious design decision we made to help prevent folks from encoding type-erased values which they would not be able to decode, without at least thinking about it.

From long-standing experience in this area — we wanted to help prevent more cases of “well, my app only ever needs to encode values, so my encoding scheme is fine” from becoming “design requirements have changed and now we need to decode the data, but because we never had the types, we can’t decode any of it”. Archived data is forever, and unless you’re aware of the tradeoffs you might be making by encoding type-erased values, you can write yourself into a hole.

2 Likes

@itaiferber Okay so this is intentional ... I can imagine the frustration indeed. No problem.
@zoul, Thanks for the quote.

Thanks you all, and thanks for taking the time. It is much more clear to me why these things are the way they are.

See you.

6 Likes