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
- saving the type of a
Connection
implementation in the implementation itself and - Using
superEncoder
andsuperDecoder
to encode/decode thefrom
andto
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
Anchor Constraints Not Honored in Xcode 10/iOS 12
Swift - Setting Calayer Bounds or Frame Not Working
How to Delay a for Loop in Swift Without Interrupting The Main Thread
Querying Geohashes in Firestore Returns Nothing
Custom Markers Disappear on Zoomin The Map and Appear on Zoomout The Map with Clustering
How to Set a Known Position and Orientation as a Starting Point of Arkit
Closure Identity in Swift: Unregister Observing Closure
Actions Assigned to Nsmenuitem Dont Seem to Work
List Is Not Conforming to Encodable
Urlcomponents Queryitems Losing Percent Encoding When Mutated
Update UIapplicationshortcutitem from Extension
The "Funk" Sound When Hitting Escape Key in App
How to Fill Triangle in Swift Using Core Graphics
Need Clarification on Typealias Syntax in Swift
Swift: Switch Statement Fallthrough Behavior
How to Find Multiple Nsrange for a String from Full String iOS Swift