Decodable Generic with Swift 4 - swift

I'm using the new Decodable protocol introduced in Swift 4.
Inside my unit test, I want to use a generic method that decodes a specific JSON file for a specific Decodable type.
I wrote the following function matching the JSONDecoder decode method:
var jsonDecoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
static let bundle: Bundle = {
let testBundle = Bundle(for: Decodable.self)
let sampleURL = testBundle.url(forResource: "api_samples", withExtension: "bundle")!
return Bundle(url: sampleURL)!
}()
static func getJSONSample(fileName: String) throws -> Data {
let url = Decodable.bundle.url(forResource: fileName, withExtension: "json")!
return try Data(contentsOf: url)
}
func assertDecode<Obj>(_ type: Obj.Type, fileName: String) where Obj: Decodable {
do {
let data = try Decodable.getJSONSample(fileName: fileName)
let _ = try jsonDecoder.decode(type, from: data)
// Same by using Obj.self, Obj.Type
} catch let error {
XCTFail("Should not have failed for \(type) with json \(fileName): \(error)")
}
}
The compiler gives me the following error:
In argument type 'Obj.Type', 'Obj' does not conform to expected type 'Decodable'
I would have imagine that Obj is decodable due to the where clause.
What is wrong with that function?

Instead of doing a "where" statement, make your life easier by restricting the generic itself:
func assertDecode<Obj: Decodable>(_ type: Obj.Type, fileName: String)

Related

Swift Decodable - How to decode nested JSON that has been base64 encoded

I am attempting to decode a JSON response from a third-party API which contains nested/child JSON that has been base64 encoded.
Contrived Example JSON
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
PS "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9" is { 'name': 'some-value' } base64 encoded.
I have some code that is able to decode this at present but unfortunately I have to reinstanciate an additional JSONDecoder() inside of the init in order to do so, and this is not cool...
Contrived Example Code
struct Attributes: Decodable {
let name: String
}
struct Model: Decodable {
let id: Int64
let attributes: Attributes
private enum CodingKeys: String, CodingKey {
case id
case attributes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let encodedAttributesString = try container.decode(String.self, forKey: .attributes)
guard let attributesData = Data(base64Encoded: encodedAttributesString) else {
fatalError()
}
// HERE IS WHERE I NEED HELP
self.attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
}
}
Is there anyway to achieve the decoding without instanciating the additional JSONDecoder?
PS: I have no control over the response format and it cannot be changed.
If attributes contains only one key value pair this is the simple solution.
It decodes the base64 encoded string directly as Data – this is possible with the .base64 data decoding strategy – and deserializes it with traditional JSONSerialization. The value is assigned to a member name in the Model struct.
If the base64 encoded string cannot be decoded a DecodingError will be thrown
let jsonString = """
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""
struct Model: Decodable {
let id: Int64
let name: String
private enum CodingKeys: String, CodingKey {
case id, attributes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let attributeData = try container.decode(Data.self, forKey: .attributes)
guard let attributes = try JSONSerialization.jsonObject(with: attributeData) as? [String:String],
let attributeName = attributes["name"] else { throw DecodingError.dataCorruptedError(forKey: .attributes, in: container, debugDescription: "Attributes isn't eiter a dicionary or has no key name") }
self.name = attributeName
}
}
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let result = try decoder.decode(Model.self, from: data)
print(result)
} catch {
print(error)
}
I find the question interesting, so here is a possible solution which would be to give the main decoder an additional one in its userInfo:
extension CodingUserInfoKey {
static let additionalDecoder = CodingUserInfoKey(rawValue: "AdditionalDecoder")!
}
var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder() //here you can put the same one, you can add different options, same ones, etc.
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
Because the main method we use from JSONDecoder() is func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable and I wanted to keep it as such, I created a protocol:
protocol BasicDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}
extension JSONDecoder: BasicDecoder {}
And I made JSONDecoder respects it (and since it already does...)
Now, to play a little and check what could be done, I created a custom one, in the idea of having like you said a XML Decoder, it's basic, and it's just for the fun (ie: do no replicate this at home ^^):
struct CustomWithJSONSerialization: BasicDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { fatalError() }
return Attributes(name: dict["name"] as! String) as! T
}
}
So, init(from:):
guard let attributesData = Data(base64Encoded: encodedAttributesString) else { fatalError() }
guard let additionalDecoder = decoder.userInfo[.additionalDecoder] as? BasicDecoder else { fatalError() }
self.attributes = try additionalDecoder.decode(Attributes.self, from: attributesData)
Let's try it now!
var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder()
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
var decoder2 = JSONDecoder()
let additionalDecoder2 = CustomWithJSONSerialization()
decoder2.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
let jsonStr = """
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""
let jsonData = jsonStr.data(using: .utf8)!
do {
let value = try decoder.decode(Model.self, from: jsonData)
print("1: \(value)")
let value2 = try decoder2.decode(Model.self, from: jsonData)
print("2: \(value2)")
}
catch {
print("Error: \(error)")
}
Output:
$> 1: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
$> 2: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
After reading this interesting post, I came up with a reusable solution.
You can create a new NestedJSONDecodable protocol which gets also the JSONDecoder in it's initializer:
protocol NestedJSONDecodable: Decodable {
init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws
}
Implement the decoder extraction technique (from the aforementioned post) together with a new decode(_:from:) function for decoding NestedJSONDecodable types:
protocol DecoderExtractable {
func decoder(for data: Data) throws -> Decoder
}
extension JSONDecoder: DecoderExtractable {
struct DecoderExtractor: Decodable {
let decoder: Decoder
init(from decoder: Decoder) throws {
self.decoder = decoder
}
}
func decoder(for data: Data) throws -> Decoder {
return try decode(DecoderExtractor.self, from: data).decoder
}
func decode<T: NestedJSONDecodable>(_ type: T.Type, from data: Data) throws -> T {
return try T(from: try decoder(for: data), using: self)
}
}
And change your Model struct to conform to NestedJSONDecodable protocol instead of Decodable:
struct Model: NestedJSONDecodable {
let id: Int64
let attributes: Attributes
private enum CodingKeys: String, CodingKey {
case id
case attributes
}
init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let attributesData = try container.decode(Data.self, forKey: .attributes)
self.attributes = try nestedDecoder.decode(Attributes.self, from: attributesData)
}
}
The rest of your code will remain the same.
You could create a single decoder as a static property of Model, configure it once, and use it for all your Model decoding needs, both externally and internally.
Unsolicited thought:
Honestly, I would only recommend doing that if you're seeing a measurable loss of CPU time or crazy heap growth from the allocation of additional JSONDecoders… they're not heavyweight objects, less than 128 bytes unless there's some trickery I don't understand (which is pretty common though tbh):
let decoder = JSONDecoder()
malloc_size(Unmanaged.passRetained(decoder).toOpaque()) // 128

generic swift object serialization?

Is there a generic way to serialize/deserialize objects for iOS? I was using the following code, and the system functions I was calling were deprecated in iOS 12:
func object(forKey:String) -> Any? {
if let data = get(BLOB_COL, forKey) {
return NSKeyedUnarchiver.unarchiveObject(with: data as! Data)
}
return nil
}
func set(_ object:Any, forKey: String) {
let data = NSKeyedArchiver.archivedData(withRootObject: object)
updateOrInsert(forKey, BLOB_COL, data)
}
It looks like the new versions of these functions requires knowledge of the object classes, and different signatures for unarchiving different collection types... is there a simple way to handle this generically?
If you would like to use NSKeyedArchiver/NSKeyedUnarchiver:
When archiving change:
NSKeyedArchiver.archivedData...
to
NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false)
And when unarchiving change
NSKeyedUnarchiver.unarchiveObject...
to
NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
Codable protocol generic approach
If you would like to use Codable to encode and decode (serialize) your Data you can extend Encodable protocol and create a method to encode any object that conforms to it:
extension Encodable {
func data(using encoder: JSONEncoder = JSONEncoder()) throws -> Data { try encoder.encode(self) }
}
And to decode the encoded data you can extend Data and create a generic method that decode any object that conforms to Decodable:
extension Data {
func object<T: Decodable>(using decoder: JSONDecoder = JSONDecoder()) throws -> T {
try decoder.decode(T.self, from: self)
}
// you can also create a string property to convert the JSON data to String
var string: String? { String(data: self, encoding: .utf8) }
}
Usage:
struct Person: Codable {
let name: String
let age: Int
}
do {
// encoding
let person = Person(name: "Joe", age: 10)
var people = [Person]()
people.append(person)
let data = try people.data()
print(data.string ?? "") // [{"name":"Joe","age":10}]
// decoding
let loadedPeople: [Person] = try data.object()
loadedPeople.forEach({print( $0.name, $0.age)}) // Joe 10
} catch {
print(error)
}
As Leo says, stop using NSKeyedArchiver and use the Codable protocol. That's the modern, Swifty way to serialize/deserialize your objects.

What type to use for generic decodable class

I have some basics in Swift, and I'm now trying to learn iOS development. I'm currently working in a small app that will ask resource on an API I've made that returns json made from :
struct A : Codable {
let name: String
let age: Int
}
struct B : Codable {
let something: String
}
Both API and app have these structs defined. As I'm always querying the same API, I thought of wrapping the part that ask the API some resources and decode this so I have an instance of the struct to use in my callback. Here's this method :
static func getContent(urlRequest: URLRequest, decodable: Decodable, completion: #escaping (Codable?, ErrorEnum?)->Void) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
data, response, error in
guard let data = data else {
completion(nil, .noData) // Handling errors in an enum
return
}
let decoder = JSONDecoder()
if let full = try? decoder.decode(decodable, from: data) {
completion(full, nil)
}
}
task.resume()
}
My problem concerns the decodable param. This shows an error and prevent me from compiling the app. After finding some resources on StackOverflow, I tried to change the parameters as
static func getContent(urlRequest: URLRequest, decodable: Decodable.Type, completion: #escaping (Codable?, ErrorEnum?)->Void)
I also tried to keep the parameter like this, and instead change inside the decode params
if let full = try? decoder.decode(decodable, from: data) {
completion(full, nil)
}
but nothing seems to satisfy the compiler... And looking at decode method inside Swift source code didn't help me that much as it requires T.Type where T is Decodable
My wish is to be able to use this as follow :
static func getA() {
guard let url = URL(string: "http://localhost/a") else { return }
let urlRequest = URLRequest(url: url)
getContent(urlRequest: urlRequest, decodable: A.self) {
a, error in
guard a = a else { return }
print(a.name!)
}
}
Do you have any idea how I could achieve this ? I also don't really know how to call this type of parameters or what to search on google that can lead me to the answer (lack of vocabulary).
Thank you !
try this just add a generic .Type of Codable and use its type as a parameter to pass foo.self
static func getContent<T: Codable>(urlRequest: URLRequest, decodable: T.Type, completion: #escaping (T?, ErrorEnum?)->Void) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
data, response, error in
guard let data = data else {
completion(nil, .noData) // Handling errors in an enum
return
}
let decoder = JSONDecoder()
if let full = try? decoder.decode(decodable, from: data) {
completion(full, nil)
}
}
task.resume()
}
You can use this:
func genericRequest<T: Decodable>(_ request: URLRequest, completion: #escaping APIGenericRequestCompletion<T>) {
Alamofire.request(request).responseData { (response) in
guard let data = response.data else {
completion(nil)
return
}
do {
let decodedObject = try JSONDecoder().decode(T.self, from: data)
completion(decodedObject)
} catch {
completion(nil)
}
}
}
where APIGenericRequestCompletion is:
typealias APIGenericRequestCompletion<T: Decodable> = (_ result: T?) -> Void
Then you use it as:
genericRequest(request) { (decodableObjectResponse) in
// your code here
}

WrapperOfNSCoding fails with single value

A question from Implementing Codable for UIColor
struct WrapperOfNSCoding<Wrapped>: Codable where Wrapped: NSCoding {
var wrapped: Wrapped
init(_ wrapped: Wrapped) { self.wrapped = wrapped }
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let data = try container.decode(Data.self)
guard let object = NSKeyedUnarchiver.unarchiveObject(with: data) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "failed to unarchive an object")
}
guard let wrapped = object as? Wrapped else {
throw DecodingError.typeMismatch(Wrapped.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "unarchived object type was \(type(of: object))"))
}
self.wrapped = wrapped
}
func encode(to encoder: Encoder) throws {
let data = NSKeyedArchiver.archivedData(withRootObject: wrapped)
var container = try encoder.singleValueContainer()
try container.encode(data)
}
}
let colors = [NSColor.red, NSColor.brown]
print(colors)
let w = WrapperOfNSCoding(colors[0])
let jsonData = try! JSONEncoder().encode(w) - fails
let jsonData = try! JSONEncoder().encode(colors.map({ WrapperOfNSCoding($0) })) - succeeds
print(jsonData)
let colors2 = try! JSONDecoder().decode([WrapperOfNSCoding<NSColor>].self, from: jsonData).map({ $0.wrapped })
print(colors2)
The error is when a single value is used in the encoder
let w = WrapperOfNSCoding(colors[0])
let jsonData = try! JSONEncoder().encode(w)
error is
Fatal error: 'try!' expression unexpectedly raised an error:
Swift.EncodingError.invalidValue(WrapperOfNSCoding #1...
This succeeds
let w = WrapperOfNSCoding([colors[0]])
let jsonData = try! JSONEncoder().encode(w)
Why would that be
JSONEncoder need valid JSON context on the top level, which could be either [:] (Dictionary) or [] (Array), inside you can place an element like in this example string.
When you save any NSCoding object, the compiler treats that object as the string. In the case of JSONEncoder().encode(w), you are trying to encode an NSCoding object which acts as a string object instead of regular JSON object.
In the case of JSONEncoder().encode([w]), the object has created an array and each element has a string which is NSCoding in your case.
In another way w data is a string and [w] is an array with each index is having a string, therefore JSONEncoder is not giving you an error with [w] and giving you an error for w.

Factory with generic protocol swift

I am creating a generic parser in my Network Layer with the help of associatedType and a Factory. Basic purpose is that I will call only one static function to whom I will pass type and data. It will do all the parsing stuff and will return me a Parsed Model Object.
protocol Parsable: Codable {
associatedtype JSON
static func Parse(object: Data) -> JSON?
}
Creation of Factory
struct ParseFactory<object: Parsable> {
let type: RequestType
func doParsing(data: Data) -> object.JSON? {
switch type {
case .RequestOne:
return ModelOne.Parse(object: data) as? object.JSON
case .RequestTwo:
return ModelTwo.Parse(object: data) as? object.JSON
}
}
}
Model Objects that create their own parsing stuff
class ModelOne: Parsable {
typealias JSON = ModelOne
let name: String
static func Parse(object: Data) -> JSON? {
let photoObject = try? JSONDecoder().decode(ModelOne.self, from: object)
return photoObject
}
}
class ModelTwo: Parsable {
typealias JSON = ModelTwo
let name: String
static func Parse(object: Data) -> JSON? {
let photoObject = try? JSONDecoder().decode(ModelTwo.self, from: object)
return photoObject
}
}
Call from Network Layer with single Line
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) -> Void in
// parsing
if let dataNotNil = data {
// _ = Parsable
}
}
Question: How can I call the one line factory function that will call the respective function of Model.
Note: Any Help would be much appreciated
Here how you can use
let session = URLSession.shared
let request = URLRequest(url: URL(string:"yoururl")!)
let task = session.dataTask(with: request) { (data, response, error) -> Void in
if let dataNotNil = data {
let objectParse = ParseFactory<ModelOne>(type: RequestType.RequestOne)
let modelOneObjc = objectParse.doParsing(data: dataNotNil)
print(modelOneObjc?.name)
}
}
Hope it is helpful