Generic Decoders in Swift 4

Generic Decoders in Swift 4

If you look at the swift stdlib for JSONEncoder and PropertyListDecoder you will see that they both share a method

func decode<T: Decodable >(_ type: T.Type, from data: Data) throws -> T

So you could create a protocol that has said method and conform both decoders to it:

protocol DecoderType {
func decode<T: Decodable >(_ type: T.Type, from data: Data) throws -> T
}

extension JSONDecoder: DecoderType { }
extension PropertyListDecoder: DecoderType { }

And create your generic parse function like so:

func parseData(_ data: Data, with decoder: DecoderType) throws ->  [ParsedType] {
return try decoder.decode([ParsedType].self, from: data)
}

Generic Decoder for Swift using a protocol

A protocol cannot conform to itself, Codable must be a concrete type or can only be used as a generic constraint.

In your context you have to do the latter, something like this

func fetch<T: Decodable>(with request: URLRequest, decode: @escaping (Data) throws -> T, completion: @escaping (Result<T, APIError>) -> Void) {  }

func getData<T: Decodable>(_ : T.Type = T.self, from endPoint: Endpoint, completion: @escaping (Result<T, APIError>) -> Void) {

let request = endPoint.request

fetch(with: request, decode: { data -> T in
return try JSONDecoder().decode(T.self, from: data)
}, completion: completion)
}

A network request usually returns Data which is more reasonable as parameter type of the decode closure

Decoding a generic decodable type

The JSON your posted isn't valid, but I'm assuming it's a typo and it's actually:

{ "id": 2, "name": "some name", "details": "some details" }
// or
{ "result": { "id": 2, "name": "some name", "details": "some details" } }

({ } instead of [ ])


Probably the cleanest is with a manual decoder that can fall back to another type, if the first type fails:

struct NetworkResponse<Wrapped> {
let result: Wrapped
}

extension NetworkResponse: Decodable where Wrapped: Decodable {
private struct ResultResponse: Decodable {
let result: Wrapped
}

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let result = try container.decode(ResultResponse.self)
self.result = result.result
} catch DecodingError.keyNotFound, DecodingError.typeMismatch {
self.result = try container.decode(Wrapped.self)
}
}
}

Alternatively, you can fall back within Combine. I would not have gone with this approach, but for completeness-sake:

URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.flatMap { data in
Just(data)
.decode(type: NetworkResponse<R>.self, decoder: decoder)
.map(\.result)
.catch { _ in
Just(data)
.decode(type: R.self, decoder: decoder)
}
}
.eraseToAnyPublisher()

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 a generic Data initializer for Decodable types using Swift

You can't overwrite class itself, but you can init it, init object from json and then assign values/ If take your code - it'll be something like this:

public class MyResponse: Codable {
public var id: String?
public var context: String?
public var results: [MyResult]?
}

public extension MyResponse {
convenience init(data: Data) throws {
self.init()
let object = try JSONDecoder().decode(MyResponse.self, from: data)
self.id = object.id
self.context = object.context
self.results = object.results
}
}

If you really don't need a class it's better to use struct instead of it, and it can be like this:

public struct MyResponse: Codable {
public let id: String?
public let context: String?
public let results: [String]?
}

public extension MyResponse {
init(data: Data) throws {
self = try JSONDecoder().decode(MyResponse.self, from: data)
}
}

Swift 4 decoding/encoding a generic data structure

You are encoding dataSource – which is [T] – but are decoding Queue, this cannot work. Try

public init(from decoder: Decoder) throws
{
print("initializing")
guard let data = userDefaults.data(forKey: "queue") else { throw Error.queueNotFound }
dataSource = try JSONDecoder().decode([T].self, from: data)
}

By the way in your code the decoded value – as well as a potential DecodingError – is unused which makes no sense.

How to reference a generic Decodable struct in Swift 4

When you want to pass a type as a parameter, you need to declare the type of the parameter as metatype. In your case, it's a generic type which needs to conform to Decodable.

So you may need to write something like this:

struct Results<Element: Decodable>: Decodable {
let items: [Element]
}
static func getResults<Element: Decodable>(url: String, parameters: Parameters?, myStruct: Element.Type) {
//...
// On success REST response
if response.result.isSuccess {

do {
let jsonResults = try JSONDecoder().decode(Results<Element>.self, from: response.data!)
//success
print(jsonResults)
} catch {
//Better not dispose errors silently
print(error)
}
}
//...
}

Swift says types cannot be nested in generic context, so I moved it out to the outer non-generic context.

Call it as:

getResults(url: "url", parameters: nil, myStruct: MyDecodableStruct.self)

Swift - Decode/encode an array of generics with different types

The following solutions resolves all the issues, that I had with generics and not knowing the specific type of Connection. The key to the solution was

  1. saving the type of a Connection implementation in the implementation itself and
  2. Using superEncoder and superDecoder to encode/decode the from and to properties.

This is the solution:

import Foundation

protocol Connection: Codable {
var type: ConnectionType { get }
var path: String { get set }
}

struct LocalConnection: Connection {
let type: ConnectionType = ConnectionType.local

var path: String
}

struct SFTPConnection : Connection {
let type: ConnectionType = ConnectionType.sftp

var path: String
var user: String
var sshKey: String

init(path: String, user: String, sshKey: String) {
self.path = path
self.user = user
self.sshKey = sshKey
}
}

struct FTPConnection: Connection {
let type: ConnectionType = ConnectionType.ftp

var path: String
var user: String
var password: String
}

struct TFTPConnection: Connection {
let type: ConnectionType = ConnectionType.tftp

var path: String
}

enum ConnectionType : Int, Codable {
case local
case sftp
case ftp
case tftp

func getType() -> Connection.Type {
switch self {
case .local: return LocalConnection.self
case .sftp: return SFTPConnection.self
case .ftp: return FTPConnection.self
case .tftp: return TFTPConnection.self
}
}
}

struct Configuration {
var from : Connection
var to : Connection
private var id = UUID.init().uuidString

var fromType : ConnectionType { return from.type }
var toType : ConnectionType { return to.type }

init(from: Connection, to: Connection) {
self.from = from
self.to = to
}
}

extension Configuration : Codable {

enum CodingKeys: String, CodingKey {
case id, from, to, fromType, toType
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.id = try container.decode(String.self, forKey: .id)

var type : ConnectionType

type = try container.decode(ConnectionType.self, forKey: .fromType)
let fromDecoder = try container.superDecoder(forKey: .from)
self.from = try type.getType().init(from: fromDecoder)

type = try container.decode(ConnectionType.self, forKey: .toType)
let toDecoder = try container.superDecoder(forKey: .to)
self.to = try type.getType().init(from: toDecoder)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(self.id, forKey: .id)

try container.encode(self.fromType, forKey: .fromType)
let fromContainer = container.superEncoder(forKey: .from)
try from.encode(to: fromContainer)

try container.encode(self.toType, forKey: .toType)
let toContainer = container.superEncoder(forKey: .to)
try to.encode(to: toContainer)
}
}


Related Topics



Leave a reply



Submit