简体   繁体   中英

Why does this model not conform to Decodable? (a polymorphic JSON Christmas tale)

Hello Codable experts.

I want to decode incoming JSON for artists data. Two types of artists exist: individuals and bands.

Individuals are represented like this:

{
  "id":"123",
  "data":{
    "type":"individual",
    "firstName":"David",
    "lastName":"Bowie"
  }
}

while bands are represented like this:

{
  "id":"124",
  "data":{
    "type":"band",
    "name":"Queen"
  }
}

I'm trying to model this JSON polymorphism (🤔 not sure this is the right word) in Swift like so:

import Foundation

struct Artist: Decodable {
    let id: String
    let data: ArtistData
}

enum ArtistType: String, Codable {
    case individual
    case band
}

protocol ArtistData: Decodable {
    var type: ArtistType { get }
}

struct IndividualArtistData: ArtistData, Codable {
    let type = ArtistType.individual
    let firstName: String
    let lastName: String
}

struct BandArtistData: ArtistData, Codable {
    let type = ArtistType.band
    let name: String
}

But I get the following error:

Type 'Artist' does not conform to protocol 'Decodable'
cannot automatically synthesize 'Decodable' because 'ArtistData' does not conform to 'Decodable'

I also tried a version where the ArtistData protocol was not inheriting Decodable and changing Artist definition to use protocol composition like so:

struct Artist: Decodable {
    let id: String
    let data: ArtistData & Decodable
}

but I get a similar error:

cannot automatically synthesize 'Decodable' because 'Decodable & ArtistData' does not conform to 'Decodable'

How should I deal with this kind of scenario?

Happy holiday season.🌲🎅

data in Artist must be a concrete type or a generic constrained to Codable . It can't be a protocol.

My suggestion is to drop the protocol and declare an enum with associated types.

enum Artist : Decodable {
    case individual(String, IndividualArtist), band(String, BandArtist)

    private enum CodingKeys : String, CodingKey { case id, data }
    private enum ArtistKeys : String, CodingKey { case type }

    init(from decoder : Decoder) throws
    {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let id = try container.decode(String.self, forKey: .id)
        let nestedContainer = try container.nestedContainer(keyedBy: ArtistKeys.self, forKey: .data)
        let type = try nestedContainer.decode(ArtistType.self, forKey: .type)
        switch type {
            case .individual:
                let individualData = try container.decode(IndividualArtist.self, forKey: .data)
                self = .individual(id, individualData)
            case .band:
                let bandData = try container.decode(BandArtist.self, forKey: .data)
                self = .band(id, bandData)
        }
    }
}

enum ArtistType : String, Decodable {
    case individual
    case band
}

struct IndividualArtist : Decodable {
    let type : ArtistType
    let firstName: String
    let lastName: String
}

struct BandArtist : Decodable {
    let type : ArtistType
    let name: String
}

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)
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM