As Protocol doesn't conform to itself?, you are getting the error:
cannot automatically synthesize Decodable
because ArtistData
does not conform to Decodable
Codable
requires all the properties of the conforming type are of concrete type or constrained to Codable
by means of generics. The type of the properties must be a full fledged type but not Protocol
. Hence your second approach doesn't work out as you expected.
Though @vadian already posted an excellent answer, I'm posting another approach which may be more inline with your thought process.
First, the Artist
type should reflect the original payload data structure:
struct Artist: Codable {
let id: String
let data: Either<BandArtist, IndividualArtist>
}
Where the Either
type is like:
enum Either<L, R> {
case left(L)
case right(R)
}
// moving Codable requirement conformance to extension
extension Either: Codable where L: Codable, R: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
// first try to decode as left type
self = try .left(container.decode(L.self))
} catch {
do {
// if the decode fails try to decode as right type
self = try .right(container.decode(R.self))
} catch {
// both of the types failed? throw type mismatch error
throw DecodingError.typeMismatch(Either.self,
.init(codingPath: decoder.codingPath,
debugDescription: "Expected either \(L.self) or \(R.self)",
underlyingError: error))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .left(left):
try container.encode(left)
case let .right(right):
try container.encode(right)
}
}
}
This should essentially solve your problem in the first place.
Now to extend this answer a little far, as I can see what you tried to achieve, where you deliberately tried to use constant value for the type
of both IndividualArtist
and BandArtist
, you need to account for the decoding part manually. Otherwise the values of type
properties here will be dumped by the JSON payload at the time of decoding (when the auto synthesized init(from decoder: Decoder)
gets called). That essentially means, if the JSON is:
{
"id":"123",
"data":{
"type":"individual",
"name":"Queen"
}
}
the JSONDecoder
still will decode this as a BandArtist
whereas this should not be the case as far as I understand like the way you are concerned. To tackle this, you need to provide custom implementation of Decodable
requirement. (@vadian's answer account for this with the nested container)
Below is how you can do it from the types themselves:
struct IndividualArtist: Codable {
let type = ArtistType.individual
let firstName: String
let lastName: String
}
extension IndividualArtist {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(ArtistType.self, forKey: .type)
guard self.type == type else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Payload doesn't match the expected value"))
}
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
}
}
struct BandArtist: Codable {
let type = ArtistType.band
let name: String
}
extension BandArtist {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(ArtistType.self, forKey: .type)
guard self.type == type else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Payload doesn't match the expected value"))
}
name = try container.decode(String.self, forKey: .name)
}
}