Swift generic Serializable protocol

Viewed 146

I noticed that I have a lot of repeated code for getting/sending JSON to my app's API, but the only thing that is different is what entity is being (de)serialized. So I came up with the following design (for brevity only the GET method):

HTTPClient.swift

func getEntityJSON<T: JSONSerializable>(
    type: T.Type,
    url: String,
    completionHandler: @escaping (_ result: T?,
                                  _ headers: [String: String]?,
                                  _ statusCode: Int?,
                                  _ error: Error?) -> Void) {
    HttpClient.sharedInstance().getJSON(url, completionHandler: { (jsonData, headers, statusCode, error) in
        if let error = error {
            completionHandler(nil, headers, statusCode, error)
        } else {
            if let entityData = jsonData {
                completionHandler(T.fromJSON(data: entityData), headers, statusCode, nil)
            }
        }
    })
}

to intend to use it like this:

HTTPClient.sharedInstance().getEntity(type: Translation, url: url) { (translation, _, _, _) in
    // do stuff with Translation instance
}

and here is the JSONSerializable protocol:

import Foundation
import SwiftyJSON


protocol Serializable: Codable {
    static func deserialize<T: Codable>(data: Data) -> T?
    func serialize() -> Data?
}

extension Serializable {
    static func deserialize<T: Decodable>(data: Data) -> T? {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
        return try? decoder.decode(T.self, from: data)
    }
    
    func serialize() -> Data? {
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        encoder.dateEncodingStrategy = .formatted(DateFormatter.iso8601Full)
        return try? encoder.encode(self)
    }
}

protocol JSONSerializable: Serializable {
    func toJSON() -> JSON?
    static func fromJSON<T>(data: JSON) -> T?
}


extension JSONSerializable {
    func toJSON() -> JSON? {
        if let data = self.serialize() {
            return try? JSON(data: data)
        } else {
            return nil
        }
    }
}

This allows me to define a struct like this:

Translation.swift


struct Translation: Hashable, Identifiable, JSONSerializable {
    var id: UUID
    ...
    
    static func fromJSON(data: JSON) -> Translation? {

and I will get serialize, deserialize, toJSON functions. But the compiler complains that Translation does not conform to JSONSerializable, due to lack of:

static func fromJSON<T>(data: JSON) -> T?. I thought I would be able to implement that function with a concrete type, Translation in this case.

I would like to be both able to do Translations.fromJSON(data: data) as well as T.fromJSON(data: data). How can I achieve this?

2 Answers

Generics (<T>) means that your method can work with any type that the caller (not the callee, not the implementer, not anyone else) specifies. In the case of fromJSON, this is clearly not the case. Any concrete implementation of fromJSON only works with T being the enclosing type, so fromJSON is not suitable to be generic.

When a method "only works with T being the enclosing type", it is the use case of Self types:

protocol JSONSerializable: Serializable {
    func toJSON() -> JSON?
    static func fromJSON(data: JSON) -> Self?
}

This way, your Translation implementation would conform to the protocol.

Similarly, deserialise should also be declared as:

static func deserialize(data: Data) -> Self?

If you actually want a JSONSerialisable that can turn JSON into any type that the caller wants, then it's less of a JSONSerialisable, and more of a JSONSerialiser, and it doesn't make much sense to inherit from Codable in that case.

OK I ended up implementing a JSONSerializer, because I couldn't get it to work for collections of items to be (de)serialized. This is how it currently looks:

import Foundation
import SwiftyJSON


protocol JSONSerializable {
    static func fromJSON(data: JSON) -> Self?
}


class JSONSerializer {
    static func deserialize<T: Decodable>(_ type: T.Type, data: Data) -> T? {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
        return try? decoder.decode(type, from: data)
    }
    
    static func serialize<T: Encodable>(_ object: T) -> Data? {
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        encoder.dateEncodingStrategy = .formatted(DateFormatter.iso8601Full)
        return try? encoder.encode(object)
    }
    
    static func fromJSON<T: JSONSerializable>(type: T.Type, data: JSON) -> T? {
        T.fromJSON(data: data)
    }
    
    static func toJSON<T: Encodable>(_ object: T) -> JSON? {
        if let data = Self.serialize(object) {
            return try? JSON(data: data)
        } else {
            return nil
        }
    }
}

I will look into the fromJSON function later to see if I can completely get rid of JSONSerializable and implemented it in the JSONSerializer like this:

    static func fromJSON<T: Decodable>(type: T.Type, data: JSON) -> T? {
        if let jsonData = try? data.rawData() {
            return Self.deserialize(type, data: jsonData)
        }
        return nil
    }
Related