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