Swift Codable struct with a generic property - ios

Say we've got a cursor based paginated API where multiple endpoints can be paginated. The response of such an endpoint is always as follows:
{
"nextCursor": "someString",
"PAYLOAD_KEY": <generic response>
}
So the payload always returns a cursor and the payload key depends on the actual endpoint we use. For example if we have GET /users it might be users and the value of the key be an array of objects or we could cal a GET /some-large-object and the key being item and the payload be an object.
Bottom line the response is always an object with a cursor and one other key and it's associated value.
Trying to make this generic in Swift I was thinking of this:
public struct Paginable<Body>: Codable where Body: Codable {
public let body: Body
public let cursor: String?
private enum CodingKeys: String, CodingKey {
case body, cursor
}
}
Now the only issue with this code is that it expects the Body to be accessible under the "body" key which isn't the case.
We could have a struct User: Codable and the paginable specialized as Paginable<[Users]> where the API response object would have the key users for the array.
My question is how can I make this generic Paginable struct work so that I can specify the JSON payload key from the Body type?

The simplest solution I can think of is to let the decoded Body to give you the decoding key:
protocol PaginableBody: Codable {
static var decodingKey: String { get }
}
struct RawCodingKey: CodingKey, Equatable {
let stringValue: String
let intValue: Int?
init(stringValue: String) {
self.stringValue = stringValue
intValue = nil
}
init(intValue: Int) {
stringValue = "\(intValue)"
self.intValue = intValue
}
}
struct Paginable<Body: PaginableBody>: Codable {
public let body: Body
public let cursor: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RawCodingKey.self)
body = try container.decode(Body.self, forKey: RawCodingKey(stringValue: Body.decodingKey))
cursor = try container.decodeIfPresent(String.self, forKey: RawCodingKey(stringValue: "nextCursor"))
}
}
For example:
let jsonString = """
{
"nextCursor": "someString",
"PAYLOAD_KEY": {}
}
"""
let jsonData = Data(jsonString.utf8)
struct SomeBody: PaginableBody {
static let decodingKey = "PAYLOAD_KEY"
}
let decoder = JSONDecoder()
let decoded = try? decoder.decode(Paginable<SomeBody>.self, from: jsonData)
print(decoded)
Another option is to always take the "other" non-cursor key as the body:
struct Paginable<Body: Codable>: Codable {
public let body: Body
public let cursor: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RawCodingKey.self)
let cursorKey = RawCodingKey(stringValue: "nextCursor")
cursor = try container.decodeIfPresent(String.self, forKey: cursorKey)
// ! should be replaced with proper decoding error thrown
let bodyKey = container.allKeys.first { $0 != cursorKey }!
body = try container.decode(Body.self, forKey: bodyKey)
}
}
Another possible option is to pass the decoding key directly to JSONDecoder inside userInfo and then access it inside init(from:). That would give you the biggest flexibility but you would have to specify it always during decoding.

You can use generic model with type erasing, for example
struct GenericInfo: Encodable {
init<T: Encodable>(name: String, params: T) {
valueEncoder = {
var container = $0.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: . name)
try container.encode(params, forKey: .params)
}
}
// MARK: Public
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
// MARK: Internal
enum CodingKeys: String, CodingKey {
case name
case params
}
let valueEncoder: (Encoder) throws -> Void
}

Related

Swift - Initialise model object with init(from decoder:)

Below is my model struct
struct MovieResponse: Codable {
var totalResults: Int
var response: String
var error: String
var movies: [Movie]
enum ConfigKeys: String, CodingKey {
case totalResults
case response = "Response"
case error = "Error"
case movies
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
self.response = try values.decodeIfPresent(String.self, forKey: .response)!
self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
}
}
extension MovieResponse {
struct Movie: Codable, Identifiable {
var id = UUID()
var title: String
var year: Int8
var imdbID: String
var type: String
var poster: URL
enum EncodingKeys: String, CodingKey {
case title = "Title"
case year = "Year"
case imdmID
case type = "Type"
case poster = "Poster"
}
}
}
Now in a ViewModel, I am creating an instance of this model using the below code
#Published var movieObj = MovieResponse()
But there is a compile error saying, call init(from decoder) method. What is the proper way to create a model instance in this case?
As the Swift Language Guide reads:
Swift provides a default initializer for any structure or class that provides default values for all of its properties and doesn’t provide at least one initializer itself.
The "and doesn’t provide at least one initializer itself" part is crucial here. Since you are declaring an additional initializer you should either declare your own initialized like so:
init(
totalResults: Int,
response: String,
error: String,
movies: [Movie]
) {
self.totalResults = totalResults
self.response = response
self.error = error
self.movies = movies
}
or move Codable conformance to an extension so Swift can provide you with a default initialiser. This would be a preferred way to do it (my personal opinion, I like to move additional protocol conformances to extensions).
struct MovieResponse {
var totalResults: Int
var response: String
var error: String
var movies: [Movie]
}
extension MovieResponse: Codable {
enum ConfigKeys: String, CodingKey {
case totalResults
case response = "Response"
case error = "Error"
case movies
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
self.response = try values.decodeIfPresent(String.self, forKey: .response)!
self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
}
}
You need to add another initializer if you do not want to use a decoder. Swift give you one for free if and only if you do not write your own initializer. Now that you have one you loose the free one.
add another:
init() {
//Your initializer code here
}
If you are trying to use the decoder init you need to use a decoder to invoke it. For instance if it's Json
#Published var movieObj = try? JSONDecoder().decode(MovieResponse.self, from: <#T##Data#>)

Is it possible to have an array of structs which conform to the same protocol also support Codable?

I have setup the following protocol, and have 2 structs which then conform to this protocol:
protocol ExampleProtocol: Decodable {
var name: String { get set }
var length: Int { get set }
}
struct ExampleModel1: ExampleProtocol {
var name: String
var length: Int
var otherData: Array<String>
}
struct ExampleModel2: ExampleProtocol {
var name: String
var length: Int
var dateString: String
}
I want to deserialise some JSON data I receive from the server, and I know it will be returning a mix of both ExampleModel1 and ExampleModel2 in an array:
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
}
Is there anyway to use the Codable approach and support both models easily? Or will I need to manually deserialise the data for each model?
EDIT 1:
Conforming to Decodable on the structs, still gives the same results:
struct ExampleModel1: ExampleProtocol, Decodable {
enum CodingKeys: String, CodingKey {
case name, length, otherData
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.length = try container.decode(Int.self, forKey: .length)
self.otherData = try container.decode(Array<String>.self, forKey: .otherData)
}
var name: String
var length: Int
var otherData: Array<String>
}
struct ExampleModel2: ExampleProtocol, Decodable {
enum CodingKeys: String, CodingKey {
case name, length, dateString
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.length = try container.decode(Int.self, forKey: .length)
self.dateString = try container.decode(String.self, forKey: .dateString)
}
var name: String
var length: Int
var dateString: String
}
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
}
If you have a limited amount of ExampleProtocols and you need to have a different type of ExampleProtocols in the same array, then you can create a holder for ExampleProtocol and use it for decoding/encoding.
ExampleHolder could hold all possible Decodable ExampleProtocol types in one array. So decoder init don't need to have so many if-else scopes and easier to add more in the future.
Would recommend keeping ExampleHolder as a private struct. So it's not possible to access it outside of file or maybe even not outside of ExampleNetworkResponse.
enum ExampleNetworkResponseError: Error {
case unsupportedExampleModelOnDecoding
}
private struct ExampleHolder: Decodable {
let exampleModel: ExampleProtocol
private let possibleModelTypes: [ExampleProtocol.Type] = [
ExampleModel1.self,
ExampleModel2.self
]
init(from decoder: Decoder) throws {
for type in possibleModelTypes {
if let model = try? type.init(from: decoder) {
exampleModel = model
return
}
}
throw ExampleNetworkResponseError.unsupportedExampleModelOnDecoding
}
}
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
enum CodingKeys: String, CodingKey {
case someString, modelArray
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
someString = try container.decode(String.self, forKey: .someString)
let exampleHolderArray = try container.decode([ExampleHolder].self, forKey: .modelArray)
modelArray = exampleHolderArray.map({ $0.exampleModel })
}
}
–––––––––––––––––––––––––––––––––
If in one response can have only one type of ExampleProtocol in the array then:
struct ExampleNetworkResponse2<ModelArrayElement: ExampleProtocol>: Decodable {
var someString: String
var modelArray: Array<ModelArrayElement>
}
usage:
let decoder = JSONDecoder()
let response = try decoder.decode(
ExampleNetworkResponse2<ExampleModel1>.self,
from: dataToDecode
)

How to use CodingKeys in sub-class when the super class is also Codable?

I'm working on JSON encode and decode, but some problems are very annoying and I don't know how to use CodingKeys in inherit classes.
I have two classes ResponseBean and ResponseWithObjectBean<T>.
Here is the response class definition:
public class ResponseBean: Codable
{
//This is only sample, I define `CodingKeys` because the property in json is in different name.
private enum CodingKeys: String, CodingKey
{
case intA
case intB
}
public var intA: Int32 = 0
public var intB: Int32 = 0
}
public class ResponseWithObjectBean<T: Codable> : ResponseBean
{
/*
Here I don't know how to define an enum to confirm protocl CondingKey.
I defined an enum named CodingKeys or whatever, they just don't work and
the testMessage and obj are still nil.
But if I implement the init(from decoder: Decoder) construction and manually
pass the coding keys which I defined to the decode function, all works fine.
*/
public var testMessage: String? = nil
public var obj: T? = nil
}
and I will get a user from the response:
public class User: Codable
{
private enum CodingKeys: String, CodingKey
{
case name
case age
}
public var name: String? = nil
public var age: Int32? = nil
}
Here is the test json:
var testJson = """
{
"intA": 10,
"intB": 20,
"testMessage": "This is a test json",
"obj":{
"name": "LiHong",
"age": 11
}
}
"""
The following is how I run:
do{
var responseData = testJson.data(using: .utf8)
var decoder = JSONDecoder()
var response: ResponseWithObjectBean<User> = try decoder.decode(ResponseWithObjectBean<User>.self, from: responseData)
}catch let e{
}
I don't know how to define CodingKeys in ResponseWithObjectBean class, and even I did, it dosen't work at all. But if I implement init(from decoder: Decoder) throws construction and manully pass coding keys I defined in ResponseWithObjectBean, I can get all properties.
This is pretty simple, you just have to do the coding and decoding by hand, in the child class:
public class ResponseWithObjectBean<T: Codable> : ResponseBean {
public var testMessage: String? = nil
public var obj: T? = nil
// Create another CodingKey compliant enum with another name for the new keys
private enum CustomCodingKeys: String, CodingKey {
case testMessage
case obj
}
// Override the decoder
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
testMessage = try container.decode(String?.self, forKey: .testMessage)
obj = try container.decode(T?.self, forKey: .obj)
}
// And the coder
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CustomCodingKeys.self)
try container.encode(testMessage, forKey: .testMessage)
try container.encode(obj, forKey: .obj)
}
}
This way you can decode and encode the way you want:
let decoder = JSONDecoder()
let response = try decoder.decode(ResponseWithObjectBean<User>.self, from: responseData)
let data = try JSONEncoder().encode(response)
print(String(data: data, encoding: .utf8))
EDIT: In order to prevent you from writing manually all this boilerplate, you can use generation tools like Sourcery: https://github.com/krzysztofzablocki/Sourcery

Swift: Constant in Template Definition

I'm working with a backend developer that likes to encapsulate json bodies in another object such as data:
Example:
GET: /user/current:
{
data: {
firstName: "Evan",
lastName: "Stoddard"
}
}
I would simply like to just call json decode on the response to get a User struct that I've created but the added data object requires another struct. To get around this I created a generic template class:
struct DecodableData<DecodableType:Decodable>:Decodable {
var data:DecodableType
}
Now I can get my json payload and if I want to get a User struct just get the data property of my template:
let user = JSONDecoder().decode(DecodableData<User>.self, from: jsonData).data
This is all fine and dandy until sometimes, the key, data, isn't always data.
I feel like this is most likely fairly trivial stuff, but is there a way I can add a parameter in my template definition so I can change the enum coding keys as that data key might change?
Something like the following?
struct DecodableData<DecodableType:Decodable, Key:String>:Decodable {
enum CodingKeys: String, CodingKey {
case data = Key
}
var data:DecodableType
}
This way I can pass in the target decodable class along with the key that encapsulates that object.
No need for coding keys. Instead, you need a simple container that parses the JSON as a dictionary that has exactly one key-value pair, discarding the key.
struct Container<T>: Decodable where T: Decodable {
let value: T
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dict = try container.decode([String: T].self)
guard dict.count == 1 else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "expected exactly 1 key value pair, got \(dict.count)")
}
value = dict.first!.value
}
}
If the JSON is empty or has more than one key-value pair, an exception is raised.
Assuming a simple struct such as
struct Foo: Decodable, Equatable {
let a: Int
}
you can parse it regardless of the key:
let foo1 = try! JSONDecoder().decode(
Container<Foo>.self,
from: #"{ "data": { "a": 1 } }"#.data(using: .utf8)!
).value
let foo2 = try! JSONDecoder().decode(
Container<Foo>.self,
from: #"{ "doesn't matter at all": { "a": 1 } }"#.data(using: .utf8)!
).value
foo1 == foo2 // true
This also works for JSON responses that have null as the value, in which case you need to parse it as an optional of your type:
let foo = try! JSONDecoder().decode(
Container<Foo?>.self,
from: #"{ "data": null }"#.data(using: .utf8)!
).value // nil
Try with something like this:
struct GenericCodingKey: CodingKey {
var stringValue: String
init(value: String) {
self.stringValue = value
}
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
struct DecodableData<DecodableType: CustomDecodable>: Decodable {
var data: DecodableType
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: GenericCodingKey.self)
data = try container.decode(DecodableType.self, forKey: GenericCodingKey(value: DecodableType.dataKey))
}
}
protocol CustomDecodable: Decodable {
static var dataKey: String { get }
}
extension CustomDecodable {
static var dataKey: String {
return "data" // This is your default
}
}
struct CustomDataKeyStruct: CustomDecodable {
static var dataKey: String = "different"
}
struct NormalDataKeyStruct: CustomDecodable {
//Change Nothing
}

Decode all properties in custom init (enumerate over all properties of a class and assign them a value)

I'm currently working on a project that it's API isn't ready yet. So sometimes type of some properties changes. For example I have this struct:
struct Animal: Codable {
var tag: Int?
var name: String?
var type: String?
var birthday: Date?
}
for this json:
{
"tag": 12,
"name": "Dog",
"type": "TYPE1"
}
But in development, the json changes to something like this:
{
"tag": "ANIMAL",
"name": "Dog",
"type": 1
}
So I get some type mismatch error in decoder and nil object. To prevent decoder from failing entire object, I implement a custom init and set nil for any unknown property and it works like charm (NOTE: I'll handle these changes later and this is only for unplanned and temporary changes):
#if DEBUG
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
tag = (try? container.decodeIfPresent(Int.self, forKey: .tag)) ?? nil
name = (try? container.decodeIfPresent(String.self, forKey: .name)) ?? nil
type = (try? container.decodeIfPresent(String.self, forKey: .type)) ?? nil
birthday = (try? container.decodeIfPresent(Date.self, forKey: .birthday)) ?? nil
}
#endif
But for larger classes and struct, I have to write any property manually and it takes time and more importantly, sometimes I missed a property to set!
So is there any way to enumerate over all properties and set them?
I know I can get all keys from container but don't know how to set it's corresponding property:
for key in container.allKeys {
self.<#corresponding_property#> = (try? container.decodeIfPresent(<#corresponding_type#>.self, forKey: key)) ?? nil
}
Thanks
The problem in your particular example is that the type of your values keeps changing: sometimes tag is a string, sometimes it's an integer. You're going to need a lot more than your Optional approach; that deals with whether something is present, not whether it has the right type. You'll need a union type that can decode and represent a string or an integer, like this:
enum Sint : Decodable {
case string(String)
case int(Int)
enum Err : Error { case oops }
init(from decoder: Decoder) throws {
let con = try decoder.singleValueContainer()
if let s = try? con.decode(String.self) {
self = .string(s)
return
}
if let i = try? con.decode(Int.self) {
self = .int(i)
return
}
throw Err.oops
}
}
Using that, I was able to decode both your examples using a single Animal struct type:
struct Animal: Decodable {
var tag: Sint
var name: String
var type: Sint
}
let j1 = """
{
"tag": 12,
"name": "Dog",
"type": "TYPE1"
}
"""
let j2 = """
{
"tag": "ANIMAL",
"name": "Dog",
"type": 1
}
"""
let d1 = j1.data(using: .utf8)!
let a1 = try! JSONDecoder().decode(Animal.self, from: d1)
let d2 = j2.data(using: .utf8)!
let a2 = try! JSONDecoder().decode(Animal.self, from: d2)
Okay, but now let's say you don't even know what the keys are going to be. Then you need an AnyCodingKey type that can mop up the keys no matter what they are, and instead of multiple properties, your Animal will have a single property that's a Dictionary, like this:
struct Animal: Decodable {
var d = [String : Sint]()
struct AnyCodingKey : CodingKey {
var stringValue: String
var intValue: Int?
init(_ codingKey: CodingKey) {
self.stringValue = codingKey.stringValue
self.intValue = codingKey.intValue
}
init(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
init(from decoder: Decoder) throws {
let con = try decoder.container(keyedBy: AnyCodingKey.self)
for key in con.allKeys {
let result = try con.decode(Sint.self, forKey: key)
self.d[key.stringValue] = result
}
}
}
So now you can decode something with complete unknown keys whose value can be string or integer. Again, this works fine against the JSON examples you gave.
Note that this is the inverse of what you originally asked to do. Instead of using the struct property names to generate the keys, I've simply accepted any key of either type and stored it flexibly in the struct through the use of the dictionary. You could also put a property façade in front of that dictionary using the new Swift 4.2 dynamicMemberLookup feature. But that is left as an exercise for the reader!
The tool you want is Sourcery. It's a meta-programming wrapper for Swift that will write the boilerplate for you, since you know what you want to write, it's just tedious (that's what Sourcery is ideal for). The important thing about Sourcery is (despite the name), there's no magic. It just writes Swift code for you based on other Swift code. That makes it easy to pull out later when you don't need it anymore.
Here is my extension.
In my case, If the type does not match, the default will be nil.
extension KeyedDecodingContainer {
public func decodeIfPresent<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T? where T: Decodable {
do {
let result = try decode(type, forKey: key)
return result
} catch {
return nil
}
}
public func decodeIfPresent(_ type: Int.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Int? {
do {
let result = try decode(type, forKey: key)
return result
} catch {
return nil
}
}
public func decodeIfPresent(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool? {
do {
let result = try decode(type, forKey: key)
return result
} catch {
return nil
}
}
public func decodeIfPresent(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String? {
do {
let result = try decode(type, forKey: key)
return result
} catch {
return nil
}
}
public func decodeIfPresent(_ type: Double.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Double? {
do {
let result = try decode(type, forKey: key)
return result
} catch {
return nil
}
}
public func decodeIfPresent(_ type: Float.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Float? {
do {
let result = try decode(type, forKey: key)
return result
} catch {
return nil
}
}

Resources