Create Codable Struct with Generic Type

Create codable struct with generic type

Use generic like below:

struct Message<T:Codable>: Codable{
var type: MessageType
var content: T
}

Swift - Codable struct with generic Dictionary var?

Since the comments already point out that Any type has nothing to do with generics, let me jump straight into the solution.

First thing you need is some kind of wrapper type for your Any attribute values. Enums with associated values are great for that job. Since you know best what types are to be expected as an attribute, feel free to add/remove any case from my sample implementation.

enum MyAttrubuteValue {
case string(String)
case date(Date)
case data(Data)
case bool(Bool)
case double(Double)
case int(Int)
case float(Float)
}

We will be later wrapping attribute values from the [String: Any] dictionary into the wrapper enum cases, but first we need to make the type conform to the Codable protocols. I am using singleValueContainer() for the decoding/encoding so the final json will produce a regular json dicts.

extension MyAttrubuteValue: Codable {

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let date = try? container.decode(Date.self) {
self = .date(date)
} else if let data = try? container.decode(Data.self) {
self = .data(data)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let float = try? container.decode(Float.self) {
self = .float(float)
} else {
fatalError()
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let string):
try? container.encode(string)
case .date(let date):
try? container.encode(date)
case .data(let data):
try? container.encode(data)
case .bool(let bool):
try? container.encode(bool)
case .double(let double):
try? container.encode(double)
case .int(let int):
try? container.encode(int)
case .float(let float):
try? container.encode(float)
}
}

}

At this point we are good to go, but before we will decode/encode the attributes, we can use some extra interoperability between [String: Any] and [String: MyAttrubuteValue] types. To map easily between Any and MyAttrubuteValue lets add the following:

extension MyAttrubuteValue {

var value: Any {
switch self {
case .string(let value):
return value
case .date(let value):
return value
case .data(let value):
return value
case .bool(let value):
return value
case .double(let value):
return value
case .int(let value):
return value
case .float(let value):
return value
}
}

init?(_ value: Any) {
if let string = value as? String {
self = .string(string)
} else if let date = value as? Date {
self = .date(date)
} else if let data = value as? Data {
self = .data(data)
} else if let bool = value as? Bool {
self = .bool(bool)
} else if let double = value as? Double {
self = .double(double)
} else if let int = value as? Int {
self = .int(int)
} else if let float = value as? Float {
self = .float(float)
} else {
return nil
}
}

}

Now, with the quick value access and new init, we can map values easily. We are also making sure that the helper properties are only available for the dictionaries of concrete types, the ones we are working with.

extension Dictionary where Key == String, Value == Any {
var encodable: [Key: MyAttrubuteValue] {
compactMapValues(MyAttrubuteValue.init)
}
}

extension Dictionary where Key == String, Value == MyAttrubuteValue {
var any: [Key: Any] {
mapValues(\.value)
}
}

Now the final part, a custom Codable implementation for MyStruct

extension MyStruct: Codable {

enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case createdDate = "createdDate"
case attributes = "attributes"
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(createdDate, forKey: .createdDate)
try container.encode(attributes.encodable, forKey: .attributes)
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
createdDate = try container.decode(Date.self, forKey: .createdDate)
attributes = try container.decode(
[String: MyAttrubuteValue].self, forKey: .attributes
).any
}

}

This solution is fairly long, but pretty straight-forward at the same time. We lose automatic Codable implementation, but we got exactly what we wanted. Now you are able to encode ~Any~ type that already conforms to Codable easily, by adding an extra case to your new MyAttrubuteValue enum. One final thing to say is that we use similar approach to this one in production, and we have been happy so far.

That's a lot of code, here is a gist.

iOS Generic type for codable property in Swift

T must also conform to Codable

struct BaseJsonStruct<T : Codable> : Codable {
let info: String
let data: T
}

Generic Codable types

I have done something similar to this in the past. Not with Firestore (although, more recently I did) but with our CMS that we use.

As @vadian pointed out, heterogeneous arrays are not supported by Swift.

Also... something else to point out.

When you have a generic type defined like...

struct Submission<Cell> {
let cells: [Cell]
}

Then, by definition, cells is a homogeneous array of a single type. If you try to put different types into it it will not compile.

You can get around this though by using an enum to bundle all your different Cells into a single type.

enum CellTypes {
case checkList(CheckListCell)
case segmented(SegmentedCell)
}

Now your array would be a homogeneous array of [CellTypes] where each element would be a case of the enum which would then contain the model of the cell inside it.

struct Submission {
let cells: [CellTypes]
}

This takes some custom decoding to get straight from JSON but I can't add that right now. If you need some guidance on that I'll update the answer.

Encoding and Decoding

Something to note from a JSON point of view. Your app will need to know which type of cell is being encoded/decoded. So your original JSON schema will need some updating to add this.

The automatic update from Firestore that you have shown is a fairly common way of doing this...

The JSON looks a bit like this...

{
"cells":
[
{
"checkListCell": {
"header": "dummy header"
}
},
{
"segmentedCell": {
"title": "dummy title"
}
}
]
}

Essentially, each item in the array is now an object that has a single key. From checkListCell, segmentedCell. This will be from any of the cases of your enum. This key tells your app which type of cell the object is.

Then the object shown against that key is then the underlying cell itself.

This is probably the cleanest way of modelling this data.

So, you might have two checklist cells and then a segmented cell and finally another checklist cell.

This will look like...

{
"cells":
[
{
"checkListCell": {
"header": "First checklist"
}
},
{
"checkListCell": {
"header": "Second checklist"
}
},
{
"segmentedCell": {
"title": "Some segmented stuff"
}
},
{
"checkListCell": {
"header": "Another checklist"
}
},
]
}

The important thing to think when analysing this JSON is not that it's harder for you (as a human being) to read. But that it's required, and actually fairly easy, for your app to read and decode/encode.

Hope that makes sense.

How to write the codable in generic format

You can do that using the following model:

struct ResponseDataModel<T: Codable>: Codable{
let data : DataClass<T>?
enum CodingKeys: String, CodingKey{
case data = "data"

}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass<T>.self, forKey:.data)
}
}

struct DataClass<T: Codable>: Codable {
let id: T?
let name: String?
let age: Int?

enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"

}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(T.self, forKey:.id)
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}

However, you should always known the type of the id property when you call decode(_:from:) function of JSONDecoder like this:

let decoder = JSONDecoder()

do {
let decoded = try decoder.decode(ResponseDataModel<Int>.self, from: data)
print(decoded)
} catch {
print(error)
}

Or you can use the following model to always map the id as Int, even if your server sends it as String:

struct ResponseDataModel: Codable{
let data : DataClass?
enum CodingKeys: String, CodingKey{
case data = "data"

}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass.self, forKey:.data)
}
}

struct DataClass: Codable {
let id: Int?
let name: String?
let age: Int?

enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"

}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
do {
id = try values.decodeIfPresent(Int.self, forKey:.id)
} catch DecodingError.typeMismatch {
if let idString = try values.decodeIfPresent(String.self, forKey:.id) {
id = Int(idString)
} else {
id = nil
}
}
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}

Create array of structs with generics

The generic argument of CacheWrapper must be a concrete type conforming to Codable. A protocol cannot conform to itself.

A solution is to create a protocol with a requirement to implement expiry (and id if necessary)

protocol Wrappable {
var id: UUID { get }
var expiry: Date { get }
}

Adopt Wrappable

struct CacheWrapper<T: Codable>: Codable, Wrappable { ...

and annotate

let foo: [Wrappable] = [a, b, c]

Then you can print

foo.forEach { print($0.expiry) }

Codable with Generics Swift 5

Instead of generics use an enum for the different types. Feel free to add more types

enum StringOrInt : Codable {
case string(String), integer(Int)

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let stringValue = try container.decode(String.self)
self = .string(stringValue)
} catch DecodingError.typeMismatch {
let integerValue = try container.decode(Int.self)
self = .integer(integerValue)
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let stringValue): try container.encode(stringValue)
case .integer(let integerValue): try container.encode(integerValue)
}
}
}

struct PostBody: Codable
{
let deviceInfo, geoLocationInfo : String
let data : Dictionary<String, StringOrInt>
}


let postBody = PostBody(deviceInfo: "Foo", geoLocationInfo: "Bar", data : ["loginIdentity" : .string("string"), "wazID" : .integer(0)])
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encodedDataDict2 = try encoder.encode(postBody)
print(String(data : encodedDataDict2, encoding : .utf8)!)

Swift Codable with Generics, holding common data from responses

What you're looking for here is a protocol.

protocol ErrorProviding {
var error: String? { get }
}

I'm intentionally changing errorDescription to error because that seems to be what you have in your root types (but you could definitely rename things here).

Then APIResponse requires that:

struct APIResponse<T: ErrorProviding> {
var value: T?
var error: String? { value?.error }
}

And then each root type with special handling implements the protocol:

extension Root1: ErrorProviding {
var error: String? { errors.first }
}

But simple root types that already have the right shape can just declare conformance with no extra implementation required.

extension Root2: ErrorProviding {}

Assuming you want more than just error, you could make this APIPayload rather than ErrorProviding and add any other common requirements.

As a side note, your code will be simpler if you just use Decodable rather than using Codable with empty encode methods. A type shouldn't conform to Encodable if it can't really be encoded.

struct with generic property conforming to Encodable in Swift

If what you've described is really what you want, it can be done without any of these type erasers. All you need is a closure. (But this assumes that Dog really exists only for encoding, as you've described, and that nothing needs value outside of that.)

struct Dog: Encodable {
// This is the key to the solution: bury the type of value inside a closure
let valueEncoder: (Encoder) throws -> Void

init<T: Encodable>(id: String, value: T) {
self.valueEncoder = {
var container = $0.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(value, forKey: .value)
}
}

enum CodingKeys: String, CodingKey {
case id, value
}

func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
}

Since value is only ever used inside of valueEncoder, the rest of the world doesn't need to know its type (Dog doesn't even need to know its type). This is what type-erasure is all about. It doesn't require making additional wrapper types or generic structs.

If you want to keep around the types like DogString and DogInt, you can do that as well by adding a protocol:

protocol Dog: Encodable {
associatedtype Value: Encodable
var id: String { get }
var value: Value { get }
}

And then make a DogEncoder to handle encoding (identical to above, except a new init method):

struct DogEncoder: Encodable {
let valueEncoder: (Encoder) throws -> Void

init<D: Dog>(_ dog: D) {
self.valueEncoder = {
var container = $0.container(keyedBy: CodingKeys.self)
try container.encode(dog.id, forKey: .id)
try container.encode(dog.value, forKey: .value)
}
}

enum CodingKeys: String, CodingKey {
case id, value
}

func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
}

Couple of kinds of dogs:

struct DogString: Dog {
let id: String
let value: String
}

struct DogInt: Dog {
let id: String
let value: Int
}

Put them in an array of encoders:

let dogs = [
DogEncoder(DogString(id: "123", value: "pop")),
DogEncoder(DogInt(id: "123", value: 123)),
]

let data = try JSONEncoder().encode(dogs)


Related Topics



Leave a reply



Submit