Make a protocol Codable and store it in an array
Asked Answered
D

3

10

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

Diatribe answered 6/5, 2018 at 23:19 Comment(5)
How about you change your Animal protocol into a class and have Cow and Duck be subclasses of itDarlenadarlene
Just make Duck and Cow Codable and remove Codable from AnimalHaslett
Has no array with type is a protocol, you can change Animal to class and subclass itTeetotaler
@LeoDabus but then Farm would not be able to be Codable since the array is not guaranteed to be Codable once Animal isn't.Diatribe
@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 youDiatribe
V
13

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

Vociferant answered 5/3, 2020 at 16:59 Comment(5)
I am missing something? where the base and payload came from?Rebarbative
payload - it's a custom CodingKey, for storing an object. But to know what kind of the object stored at the payload key, we should store the type of the object at the base key. You can choose names whatever you want.Vociferant
thanks for the explanation. Didn´t have any idea of that.Rebarbative
@Vociferant - love the second enum solution. By the way, I wonder if the extension with explicit decoding is still necessary as Cow and Duck are already conforming Codable. If my understanding is correct, it'd better get rid of the extension.Andro
@MatthewSo That's right. As of Swift 5.5 this is no longer requiredVociferant
I
3

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")
}
Inscrutable answered 26/11, 2020 at 15:13 Comment(0)
J
1

Here's an additional method utilizing a property wrapper. This approach is highly compatible with class-based models. However, for structs, it's necessary to register them using the following code snippet: TypeHelper.register(type: MyTypeModel.self)

import Foundation

protocol MainCodable: Codable {}

extension MainCodable {
    static var typeName: String { String(describing: Self.self) }
    var typeName: String { Self.typeName }
}


/// Convert string to type. didn't find way to convert non reference types from string
/// You can register any type by using register function
struct TypeHelper {
    private static var availableTypes: [String: Any.Type] = [:]
    private static var module = String(reflecting: TypeHelper.self).components(separatedBy: ".")[0]

    static func typeFrom(name: String) -> Any.Type? {
        if let type = availableTypes[name] {
            return type
        }
        return _typeByName("\(module).\(name)")
    }

    static func register(type: Any.Type) {
        availableTypes[String(describing: type)] = type
    }
}

@propertyWrapper
struct AnyMainCodable<T>: Codable, CustomDebugStringConvertible {
    private struct Container: Codable, CustomDebugStringConvertible {
        let data: MainCodable

        enum CodingKeys: CodingKey {
            case className
        }

        init?(data: Any) {
            guard let data = data as? MainCodable else { return nil }
            self.data = data
        }

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            let name = try container.decode(String.self, forKey: .className)

            guard let type = TypeHelper.typeFrom(name: name) as? MainCodable.Type else {
                throw DecodingError.valueNotFound(String.self, .init(codingPath: decoder.codingPath, debugDescription: "invalid type \(name)"))
            }
            data = try type.init(from: decoder)
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(data.typeName, forKey: .className)
            try data.encode(to: encoder)
        }

        var debugDescription: String {
            "\(data)"
        }
    }

    var wrappedValue: [T] {
        get { containers.map { $0.data as! T } }
        set { containers = newValue.compactMap({ Container(data: $0) }) }
    }

    private var containers: [Container]

    init(wrappedValue: [T]) {
        if let item = wrappedValue.first(where: { !($0 is MainCodable) }) {
            fatalError("unsupported type: \(type(of: item)) (\(item))")
        }
        self.containers = wrappedValue.compactMap({ Container(data: $0) })
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.containers = try container.decode([Container].self)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(containers)
    }

    var debugDescription: String {
        "\(wrappedValue)"
    }
}

Example

protocol Proto: MainCodable {
    var commData: String { get }
}

class A: Proto {
    var someData: Int
    var commData: String

    init(someData: Int, commData: String) {
        self.someData = someData
        self.commData = commData
    }
}

class B: Proto {
    var theData: String
    var commData: String

    init(theData: String, commData: String) {
        self.theData = theData
        self.commData = commData
    }
}

struct C: MainCodable {
    let cValue: String
    init(cValue: String) {
        self.cValue = cValue
    }
}
// For struct need to register every struct type you have to support
TypeHelper.register(type: C.self)


struct Example: Codable {
    @AnyMainCodable var data1: [Proto]
    @AnyMainCodable var data2: [MainCodable]
    var someOtherData: String
}

let example = Example(
    data1: [A(someData: 10, commData: "my Data1"), B(theData: "20", commData: "my Data 2")],
    data2: [A(someData: 30, commData: "my Data3"), C(cValue: "new value")],
    someOtherData: "100"
)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let decoder = JSONDecoder()
var data = try encoder.encode(example)
print(String(data: data, encoding: .utf8) ?? "")
print(try decoder.decode(type(of: example), from: data))
print(example.data1.map(\.commData))

output

{
  "data1" : [
    {
      "className" : "A",
      "someData" : 10,
      "commData" : "my Data1"
    },
    {
      "className" : "B",
      "theData" : "20",
      "commData" : "my Data 2"
    }
  ],
  "data2" : [
    {
      "className" : "A",
      "someData" : 30,
      "commData" : "my Data3"
    },
    {
      "className" : "C",
      "cValue" : "new value"
    }
  ],
  "someOtherData" : "100"
}
Example(_data1: [PlaygroundCLI.A, PlaygroundCLI.B], _data2: [PlaygroundCLI.A, PlaygroundCLI.C(cValue: "new value")], someOtherData: "100")
["my Data1", "my Data 2"]
Jestude answered 24/4, 2023 at 15:16 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.