ngngngn
ngngngn

Reputation: 579

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

Upvotes: 10

Views: 7414

Answers (3)

Alex
Alex

Reputation: 1601

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"]

Upvotes: 0

Paul B
Paul B

Reputation: 5125

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

Upvotes: 2

nightwill
nightwill

Reputation: 818

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

Upvotes: 15

Related Questions