I have a struct which contains a property. This property's type can vary, but will conform to a specified protocol.
Problem is the struct itself won't conform to Decodable.
Here's some basic code.
protocol MyProtocol: Decodable {}
struct MyStruct: Decodable
{
var item: MyProtocol // Type 'MyStruct' does not conform to protocol 'Decodable'
}
Or if I expand that to full implementation it becomes…
protocol MyProtocol: Decodable {}
struct MyStruct: Decodable
{
var item: MyProtocol
enum CodingKeys: CodingKey
{
case item
}
init(from decoder: Decoder) throws
{
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
item = try container.decode(MyProtocol.self, forKey: .item) // Type 'any MyProtocol' cannot conform to 'Decodable'
}
}
In the expanded version I realise that the init(from:) method won't know what specific type to create. Is there any way around this?
I'm able to get the functionality I want by using generics, however in the real code the property in question is optional and it feels wrong to specify a "phantom" generic type for the struct when the generic type isn't needed.
struct MyStruct: Decodable {
enum CodingKeys: CodingKey {
case item
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let v = try? container.decode(Int.self, forKey: .item) {
item = v
} else if let v = try? container.decode(Double.self, forKey: .item) {
item = v
} else if let v = try? container.decode(String.self, forKey: .item) {
item = v
} else {
fatalError("TODO other types")
}
}
var item: Decodable
}
Although to me this looks wrong and generics is the right (and simpler) approach here.
A type doesn't conform to a protocol because all of its members conform. It conforms when it implements the init(from:) requirement.
So, yes, a type with an existential property can conform to Decodable. You just have to provide an initializer that does what you want it to do.
If you're asking whether a type with an existential property can have its conformance synthesized, then the answer is obviously: no. I say "obviously", because I have no idea from the information you've given what concrete type you want, so I wouldn't expect the compiler to know either.
Right, the difficulty the compiler has in synthesizing an implementation here is exactly the same problem you've run into yourself when trying to write the implementation manually:
The design of Decodable expects that types know how to construct an instance of themselves from a given Decoder. In the case where every stored property is itself Decodable, the problem is trivial because you can just have each property instantiate itself recursively. But MyProtocol doesn't have a concrete implementation of init(from:) that the Decodable machinery can call to produce a value of type MyProtocol.
Based on this, it sounds like in every context where you're actually decoding a MyStruct instance, you do in fact have the information needed to know what concrete type will be stored in item. If that's the case, it sounds like while MyStruct can't reasonably conform to Decodable itself, you could have a wrapper type which provides the concrete type information to produce a MyStruct value, something like:
import Foundation
protocol MyProtocol: Decodable {}
extension Int: MyProtocol {}
struct MyStruct {
var item: MyProtocol
}
@propertyWrapper
struct MyStructDecoder<T: MyProtocol>: Decodable {
var wrappedValue: MyStruct
enum CodingKeys: CodingKey {
case item
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let item = try container.decode(T.self, forKey: .item)
wrappedValue = MyStruct(item: item)
}
}
struct SomeType: Decodable {
@MyStructDecoder<Int>
var myStruct: MyStruct
}
let json = "{ \"myStruct\": { \"item\": 3 } }"
let someType = try! JSONDecoder().decode(SomeType.self, from: json.data(using: .utf8)!)
print(someType.myStruct) // MyStruct(item: 3)
After posting the question, it stuck in my mind (as @QuinceyMorris points out) that for this to work the compiler would need to know what type to create – I'd need to feed that in at some point.
As a side note, doing the same sort of thing but using Encodable is fine, because we already have the type to be encoded.
As @tera mentions, one way of approaching this is to manually check known possibilities. This could work in my case (as there's only 3 current options).
In the end, just after posting the original question, I reverted back to using generics and created an "empty" type to specify when the type isn't required. This way lets the call site reflect the absence of a value.
So in the end, thanks to @tera for providing an answer which achieves the task given. Possibly @Jumhyn too (unfortunately I couldn't grasp the code fully). However his post does highlight that the call site does know what type to expect (as with generics) and that can be used to guide the appropriate version.
So thanks to all 3 of you. I've often found programming (and computers in general) is like one massive game of catch-22. It's one thing to write correct syntax. It's another to transcribe how we (as humans) would solve a problem.