“Don’t Repeat Yourself”

Before Codable, converting a JSON dictionary into an object in Swift required a manual conversion:

self.address = dictionary["address"] as? String
Copy the code

Even if you use a library like SwiftyJSON, just to make writing easier, you still need to specify the type manually.

Self.address specifies the type when it is declared, and the type when it is extracted, which is completely redundant. With generics, we can omit this step altogether:

extension Dictionary {
    func mapValue<T>(key: Key) -> T? {
        return self[key] as? T}}// let address: String
self.address = dictionary.mapValue("address")  / /?? "defaultValue"
Copy the code

The generic T in mapValue here is determined by the return value type, which varies with the assignment object type. In this way, we omit the manual cast and focus more on the mapping.

More complex types

In the above implementation, the raw value is simply cast, not suitable for more complex types (such as custom Objects). Let’s keep improving.

For the Custom Object, we can specify that it conforms to a Protocol that has an initialization method in it.

protocol JSONInitializable {
    init? (dictionary: [String: Any])}Copy the code

This way we can extend the above code

extension Dictionary {
    func mapValue<T>(key: Key) -> T? {
        return self[key] as? T
    } 
    
    func mapValue<T: JSONInitializable>(key: Key) -> T? {
        if let value = self[key] as? [String: Any] {
            return T.init(dictionary: value)
        }
        return nil}}Copy the code

For JSONInitializable types, the compiler automatically uses the second method.

With only a few lines of code, our parsing library is already in shape. The complete usage is as follows:

class Person: JSONInitializable {
    let name: String
	let contect: Contact?
    
    required init? (dictionary: [String: Any]) {
     	name = dictionary.mapValue("phone") // ?? "" 
        contact = dictionary.mapValue("contact")}}struct Contact: JSONInitializable {
    let phone: String
    let email: String
    let address: String
    
    init? (dictionary: [String: Any]) {
     	phone = dictionary.mapValue("phone")   
        / /...}}Copy the code

“Build the Whole World”

The above implementation still does not easily support all cases, such as container class types (Array, Dictionary). This problem is easy to handle, we just need to implement the JSONInitializable protocol on the container types themselves, and they can be combined in infinite ways.

But first, let’s tune the JSON data structure. Since JSON is not only a dictionary, but also an Array, using [String:Any] to represent JSON and Array is not acceptable. For simplicity, we used Any to represent JSON (so that it fits seamlessly with the JSON data structure parsed by JSONSerialization). But since Any is too broad and cannot be extended, we add a little wrapper:

struct JSON {
    private value: Any
    
    subscript(key: String) - >Any? {
        return value[key]
    }
    var rootValue: Any {
        return value
    }
}

// JSONInitializable corresponding change
protocol JSONInitializable {
    init? (JSON: JSON)}// mapValue methods are declared in JSON data structures
// The method implementation has not changed substantially
extension JSON {
    func mapValue<T>(key: Key) -> T? {... }func mapValue<T: JSONInitializable>(key: Key) -> T? {... }}Copy the code

This allows us to extend Array:

extension Array: JSONInitializable where Element: JSONInitializable {

    public init? (JSON: JSON) {
        guard let values = JSON.rootValue as? [Any]) {
            return nil
        }
        self = values.map {
            return Element.init(JSON: JSON(value:$0))}}}Copy the code

Similarly, you can extend the rest of the container types. It’s worth noting that Optional is also a container type.

Either [Int], [[Int]], or [String:[Person]]? . Endless combinations can be tried.

Our JSON parsing library is also complete. It has very little code, but it’s very powerful:

  • You only need to focus on the mapping; you don’t need to manually specify the type
  • Compatible not only with basic types, but also with custom types, and their various combinations

More and more

The code above is simple for demonstration purposes, but there are still many ways to enrich it:

  • Error handling: If a key is missing, or the data format does not match, simply return nil. You can set initializer to bethrowsThrow error when an error occurs, and let the upper layer decide what to do.
  • Nested key: The SUBscript method of JSON can be easily extended to support key path.
  • Default custom conversions: You can extend basic type support to JSONInitializable, making it compatible with initialization from other data type formats, such as String to Int, Double to Date, etc.
  • Omitting the mapping? : Although the Swift class can also get the name of a property through reflection, the let Property compiler requires manual assignment during initialization, so automatic mapping is still not possible.varAnd nullable type? No, that’s not Swift.

A production-level implementation of the above exists here and is welcome to use:

leavez/Mappable

JSON to Model converter, specially optimized for immutable properties

About Codable

This library is a bit primitive in comparison to Swift 4’s Codable library, which also excludes mapping and uses property directly. But Codable has one big limitation in Swift 4: it doesn’t support subclass initialization. When the parent class is already Codable, the automatic mapping for subclasses is completely disabled and must be handled manually, which is cumbersome to implement. Although Swift advocates the use of structs and composition to declare models, there is still a lot of room for the traditional OOP approach of class inheritance in development. This also leaves room for such primitive libraries.

But is manually specifying mappings necessarily a bad thing? I don’t think so. I used an auto-mapping library for medium to large projects in the ObjC era, and at that time I still insisted on explicitly specifying the mapping manually. Writing code this way makes it clear what is network data and what is local data. Also, forcing the mapping to be specified manually gives the (casual) programmer an opportunity to improve the naming of the property, rather than exactly the same as the key in JSON, which often doesn’t apply to the client.