8
votes

I have the Animal protocol with 2 structs that conform to it and a Farm struct which stores a list of Animals. Then, I make them all conform to Codable to store it in a file, but it throws the error cannot automatically synthesize 'Encodable' because '[Animal]' does not conform to 'Encodable'

I understand why this happens, but I cannot find a good solution. How can I make the array only accept Codable and Animal, without Animal being marked Codable so this issue does not happen, something like var animals = [Codable & Animal]? (or any other work arounds). Thank you

protocol Animal: Codable {
    var name: String { get set }
    var sound: String { get set }
}

struct Cow: Animal {
    var name = "Cow"
    var sound = "Moo!"
}

struct Duck: Animal {
    var name = "Duck"
    var sound = "Quack!"
}

struct Farm: Codable {

    var name = "Manor Farm"
    // this is where the error is shown
    var animals = [Animal]()

}

--edit-- When I change them to a class, it looks like this:

class Animal: Codable {
    var name = ""
    var sound = ""
}

class Duck: Animal {
    var beakLength: Int

    init(beakLength: Int) {
        self.beakLength = beakLength
        super.init()

        name = "Duck"
        sound = "Quack!"
    }

    required init(from decoder: Decoder) throws {
        // works, but now I am required to manually do this?
        fatalError("init(from:) has not been implemented")
    }
}

It would work if I had no additional properties, but once I add one I am required to introduce an initializer, and then that requires I include the init from decoder initializer which removes the automatic conversion Codable provides. So, either I manually do it for every class I extend, or I can force cast the variable (like var beakLength: Int!) to remove the requirements for the initializers. But is there any other way? This seems like a simple issue but the work around for it makes it very messy which I don't like. Also, when I save/load from a file using this method, it seems that the data is not being saved

2
How about you change your Animal protocol into a class and have Cow and Duck be subclasses of it - Nader Besada
Just make Duck and Cow Codable and remove Codable from Animal - Leo Dabus
Has no array with type is a protocol, you can change Animal to class and subclass it - Quoc Nguyen
@LeoDabus but then Farm would not be able to be Codable since the array is not guaranteed to be Codable once Animal isn't. - ngngngn
@NaderBesada I tried this first, forgot to mention. I will update the post with why it is not a perfect solution and why I am seeing if there is anything else. Thank you - ngngngn

2 Answers

4
votes

You can do this in 2 ways:

1 Solution - with Wrapper:

protocol Animal {}

struct Cow: Animal, Codable {
}

struct Duck: Animal, Codable {
}

struct Farm: Codable {
    let animals: [Animal]

    private enum CodingKeys: String, CodingKey {
        case animals
    }

    func encode(to encoder: Encoder) throws {
        let wrappers = animals.map { AnimalWrapper($0) }
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(wrappers, forKey: .animals)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let wrappers = try container.decode([AnimalWrapper].self, forKey: .animals)
        self.animals = wrappers.map { $0.animal }
    }
}

fileprivate struct AnimalWrapper: Codable {
    let animal: Animal

    private enum CodingKeys: String, CodingKey {
        case base, payload
    }

    private enum Base: Int, Codable {
        case cow
        case duck
    }

    init(_ animal: Animal) {
        self.animal = animal
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let base = try container.decode(Base.self, forKey: .base)

        switch base {
            case .cow:
                self.animal = try container.decode(Cow.self, forKey: .payload)
            case .duck:
                self.animal = try container.decode(Duck.self, forKey: .payload)
        }
    }

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

        switch animal {
            case let payload as Cow:
                try container.encode(Base.cow, forKey: .base)
                try container.encode(payload, forKey: .payload)
            case let payload as Duck:
                try container.encode(Base.duck, forKey: .base)
                try container.encode(payload, forKey: .payload)
            default:
                break
        }
    }
}

2 Solution - with Enum

struct Cow: Codable {
}

struct Duck: Codable {
}

enum Animal {
    case cow(Cow)
    case duck(Duck)
}

extension Animal: Codable {
    private enum CodingKeys: String, CodingKey {
        case base, payload
    }

    private enum Base: Int, Codable {
        case cow
        case duck
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let base = try container.decode(Base.self, forKey: .base)
        switch base {
            case .cow:
                self = .cow(try container.decode(Cow.self, forKey: .payload))
            case .duck:
                self = .duck(try container.decode(Duck.self, forKey: .payload))
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
            case .cow(let payload):
                try container.encode(Base.cow, forKey: .base)
                try container.encode(payload, forKey: .payload)
            case .duck(let payload):
                try container.encode(Base.duck, forKey: .base)
                try container.encode(payload, forKey: .payload)
        }
    }
}

1
votes

Personally I would opt to @nightwill enum solution. That's how seems to be done right. Yet, if you really need to encode and decode some protocol constrained objects that you don't own, here is a way:

protocol Animal {
    var name: String { get set }
    var sound: String { get set }
    //static var supportedTypes : CodingUserInfoKey { get set }
}

typealias CodableAnimal = Animal & Codable
struct Cow: CodableAnimal  {
    var name = "Cow"
    var sound = "Moo!"
    var numberOfHorns : Int = 2 // custom property
    // if you don't add any custom non optional properties you Cow can easyly be decoded as Duck
}

struct Duck: CodableAnimal {
    var name = "Duck"
    var sound = "Quack!"
    var wingLength: Int = 50 // custom property
}

struct Farm: Codable {
    
    var name  = "Manor Farm"
    var animals = [Animal]()
    
    enum CodingKeys: String, CodingKey {
        case name
        case animals
    }
    func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: CodingKeys.self)
        try c.encode(name, forKey: .name)
        var aniC = c.nestedUnkeyedContainer(forKey: .animals)
        for a in animals {
            if let duck = a as? Duck {
                try aniC.encode(duck)
            } else if let cow = a as? Cow {
                try aniC.encode(cow)
            }
        }
    }
    
    
    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        name = try c.decode(String.self, forKey: .name)
        var aniC = try c.nestedUnkeyedContainer(forKey: .animals)
        while !aniC.isAtEnd {
            if let duck = try? aniC.decode(Duck.self) {
                animals.append(duck)
            } else if let cow = try? aniC.decode(Cow.self) {
                animals.append(cow)
            }
        }
    }
    
    init(name: String, animals: [Animal]) {
        self.name = name
        self.animals = animals
    }
}

Playground quick check:

let farm = Farm(name: "NewFarm", animals: [Cow(), Duck(), Duck(), Duck(name: "Special Duck", sound: "kiya", wingLength: 70)])

print(farm)
import Foundation
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let encodedData = try! jsonEncoder.encode(farm)
print(String(data: encodedData, encoding: .utf8)!)
if let decodedFarm = try? JSONDecoder().decode(Farm.self, from: encodedData) {
    print(decodedFarm)
    let encodedData2 = try! jsonEncoder.encode(decodedFarm)
    print(String(data: encodedData2, encoding: .utf8)!)
    assert(encodedData == encodedData2)
} else {
    print ("Failed somehow")
}