0

I have the following Codable protocol containing a variable which I would like to exclude from the codable ones.

Problem is that I can't use the CodingKeys enum made for that within my own protocol: Type 'CodingKeys' cannot be nested in protocol 'Animal'.

protocol Animal: Codable {

    var name: String { get set }
    var color: String { get }

    var selfiePicture: Selfie { get }

    // Not possible
    enum CodingKeys: String, CodingKey {
        case name
        case color
    }

}

How could I resolve this?


EDIT with more code and more specific example

Animal is used by several structs (can't be classes):

struct Frog: Animal {
    var name: String
    var color: String

    // extra variables on top of Animal's ones
    var isPoisonous: Bool

    var selfiePicture = [...]
}

It is also used as a variable array on another top-codable object:

final class Farm: Codable {

    var address: String
    // more variables
    var animals: [Animal]

    enum CodingKeys: String, CodingKey {
        case address
        case animals
    }

    convenience init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        address = try values.decode(String.self, forKey: .address)
        animals = try values.decode([Animal].self, forKey: .animals)   // ERROR --> Protocol 'Animal' as a type cannot conform to 'Decodable'
    }
}
Tulleb
  • 8,919
  • 8
  • 27
  • 55
  • Have a look here for nested types in protocols: https://stackoverflow.com/questions/31845066/nested-types-inside-a-protocol – Teetz Jan 14 '22 at 13:17
  • You can share the keys if you use a `class` and implement `Codable` manually. then any other classes that share can inherit the `class` with the keys – lorem ipsum Jan 14 '22 at 13:56
  • @Iorem I can't since `MyProtocol` is used by structs and not classes. – Tulleb Jan 14 '22 at 15:22
  • I edited my question for more precisions. – Tulleb Jan 14 '22 at 15:32
  • You don't need a protocol, use `struct Animal` and add a property `type` that could be an enum of all your types of animals – Joakim Danielson Jan 14 '22 at 15:44
  • @JoakimDanielson I'd love to but `Animal` "sub-struct" have their own variables on top of the `Animal` ones (cf. `isPoisonous` from `struct Frog`). That's why I choose `protocol` here (https://stackoverflow.com/questions/48819953/alternate-approach-to-inheritance-for-swift-structs). – Tulleb Jan 14 '22 at 17:56
  • Ok I understand. Are the extra properties also Codable? – Joakim Danielson Jan 14 '22 at 18:54
  • @JoakimDanielson Yes. I can just add 1 variable per Animal struct type into my `Farm` object, but I was expecting a cleaner way to do it. – Tulleb Jan 15 '22 at 00:35
  • Sorry but I don’t understand that last comment – Joakim Danielson Jan 15 '22 at 07:51
  • Yes @JoakimDanielson, I need the extra properties from `Frog` to be Codable as well. I just saw your answer was edited. I am taking a look at it now, thank you :) – Tulleb Jan 15 '22 at 13:43

2 Answers2

2

One way to solve this is to use composition, move the common properties to a new type and use that type in the protocol.

So let's make a type for the common properties and let that type hold the CodingKey enum

struct AnimalCommon: Codable {
    var name: String
    var color: String

    var selfiePicture: Selfie = Selfie()

    enum CodingKeys: String, CodingKey {
        case name
        case color
    }
}

And the protocol becomes

protocol Animal: Codable {
    var common: AnimalCommon { get set }
}

After that it will be quite easy to implement the actual Animal types, for example

struct Frog: Animal {
    var common: AnimalCommon
    var isPoisonous: Bool
}

let frog = Frog(common: AnimalCommon(name: "kermit", color: "green"), isPoisonous: false)
do {
    let data = try JSONEncoder().encode(frog)

    if let string = String(data: data, encoding: .utf8) { print(frog) }
} catch {
    print(error)
}

You can also add an extension to the protocol with computed properties so you can access the properties directly, i.e frog.name = "Kermit"

extension Animal {
    var name: String {
        get {
            common.name
        }
        set {
            common.name = newValue
        }
    }

    var color: String {
        common.color
    }
}
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • Problem is that I use the MyProtocol from another final class containing `var protocolStructs: [MyProtocol]`, which needs MyProtocol to be `Codable`. – Tulleb Jan 14 '22 at 14:19
  • @Tulleb But MyProtocol is conforming to `Codable` – Joakim Danielson Jan 14 '22 at 14:24
  • I have the error `Protocol 'MyProtocol' as a type cannot conform to 'Decodable'` when decoding `protocolStructs`. I'll edit my question with more informations. – Tulleb Jan 14 '22 at 15:20
  • Thank you for your edit @JoakimDanielson, I'm trying it now. It looks great! – Tulleb Jan 15 '22 at 13:46
  • Looks like Swift does not want to decode protocols: I still have the `Protocol 'Animal' as a type cannot conform to 'Decodable'` error (line `animals = try values.decode([Animal].self, forKey: .animals)`). – Tulleb Jan 15 '22 at 14:28
  • I just saw this topic: https://stackoverflow.com/questions/57026463/protocol-type-cannot-conform-to-protocol-because-only-concrete-types-can-conform. I'll try to replace the `protocol` with some kind of `enum` and keep you updated. – Tulleb Jan 15 '22 at 14:29
0

Following Protocol type cannot conform to protocol because only concrete types can conform to protocols, I had to give up on the protocol and use a struct + enum inside instead.

Even though @JoakimDanielson's answer was promising, it does not fix an error I have while trying to decode the Animal array from my Farm class: Protocol 'Animal' as a type cannot conform to 'Decodable'.

Here is how my model looks like at the end:

struct Animal: Codable {

    enum Species: Codable {
        case frog(FrogSpecificities)
        case ...

        var selfiePicture: Selfie {
            switch self {
            case .frog(let frogSpecificities):
                return frogSpecificities.selfie
            case ...
                ...
            }
        }

        enum CodingKeys: String, CodingKey {
            case FrogSpecificities
            case ...
        }

        init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)

            if let frogSpecificities = try? values.decode(FrogSpecificities.self, forKey: .frogSpecificities) {
                self = .frog(frogSpecificities)
            } else if ...
                ...
            } else {
                // throw an error here if no case was decodable
            }
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)

            switch self {
            case .frog(let frogSpecificities):
                try container.encode(frogSpecificities, forKey: .frogSpecificities)
            case ...:
                ...
            }
        }
    }

    var name: String
    let color: String

    var species: Species

    enum CodingKeys: String, CodingKey {
        case name
        case color
        case specificities
    }

}

struct FrogSpecificities: Codable {

    let isPoisonous: Bool
    let selfie = Selfie()

    enum CodingKeys: String, CodingKey {
        case isPoisonous
    }

}

final class Farm: Codable {

    var address: String
    var animals: [Animal]

    enum CodingKeys: String, CodingKey {
        case address
        case animals
    }

    convenience init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        address = try values.decode(String.self, forKey: .address)
        animals = try values.decode([Animal].self, forKey: .animals) // Works fine
    }
}

My Farm object can now contains an Animal arrays with specific codable struct for each one of them. It can also contains variables which are not codable. I can access each specificities of my animals like this:

if let firstAnimel = MyFarm.animals.firstObject,
    case .frog(let frogSpecificities) = firstAnimal.species {
    print(frogSpecificities.isPoisonous)
}
Tulleb
  • 8,919
  • 8
  • 27
  • 55