Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make a protocol Codable and store it in an array

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

like image 443
ngngngn Avatar asked May 06 '18 23:05

ngngngn


People also ask

What is Codable protocol?

Codable is the combined protocol of Swift's Decodable and Encodable protocols. Together they provide standard methods of decoding data for custom types and encoding data to be saved or transferred.

Is Swift a Codable array?

A lot of Swift's built-in types already conform to Codable by default. For example, Int , String , and Bool are Codable out of the box. Even dictionaries and arrays are Codable by default as long as the objects that you store in them conform to Codable .

Can a protocol conform to Codable Swift?

As evident from Swift open source code, a Codable type is a type which conforms to both Decodable and Encodable protocols. It is important to note that it is not mandatory to use Codable every time for all the use-cases unless your requirement is to both serialize and de-serialize JSON response.


2 Answers

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

like image 65
nightwill Avatar answered Oct 26 '22 23:10

nightwill


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")
}
like image 43
Paul B Avatar answered Oct 27 '22 01:10

Paul B