Skip to content
This repository has been archived by the owner on Feb 8, 2021. It is now read-only.

Latest commit

 

History

History
402 lines (266 loc) · 11.9 KB

GLOSS_CODABLE_MIGRATION_GUIDE.md

File metadata and controls

402 lines (266 loc) · 11.9 KB

Gloss → Codable Migration Guide

Adopters of Gloss are urged to migrate to Swift's first-class JSON parsing framwork, Codable. Gloss is no longer maintainined as of September 2020, unless in support of Codable migration. For more context on this decision, see: hkellaway.github.io/blog/2020/08/30/tale-of-third-parties

🚨 Before Migration 🚨

Upgrade Gloss to minimum version 3.2.0

Version 3.2.0 adds methods to help translate between Gloss and Codable models.

Migration Summary

The following is a summary of steps to take when migrating any single model from using Gloss to using Codable.

Comparable Terms

Gloss Codable
JSONDecodable Decodable
JSONEncodable Encodable
Glossy Codable
Gloss.Decoder Swift.Decoder
Gloss.Encoder Swift.Encoder

Migration Steps

Step 1: Update your Gloss model to add Codable conformance

If your Gloss model conforms to JSONDecodable, add conformance to Decodable. A model that looks like this:

import Gloss

struct MyModel: JSONDecodable {

    let id: Int?
    
    init?(json: JSON) {
        self.id = "id" <~~ json
    }

}

adds

extension MyModel: Decodable { }

Alternatively, the following could be added to silence Codable errors:

extension My Model: Decodable {

    init(from decoder: Swift.Decoder) throws {
        throw GlossError.decodableMigrationUnimplemented(context: "TODO")
    }

}

Similarly, JSONEncodable models add conformance to Encodable:

struct MyModel: Encodable { }

alternatively:

extension MyModel: Encodable {

    func encode(to encoder: Swift.Encoder) throws {
        throw GlossError.encodableMigrationUnimplemented(context: "TODO")
    }

}

NOTE: Explicit usage of Swift.Decoder and Swift.Encoder is needed not to namespace clash with Gloss.Decoder and Gloss.Encoder.

Step 2: Update call-sites

At call-sites where the Gloss model is used, update from using the current Gloss methods for decoding or encoding to new ones that take Codable into account.

For example, where initializing that model currently looks like:

let myModel = MyModel(json: someJSON)

it becomes:

let myModel: MyModel? = .from(decodableJSON: someJSON)

As for encoding, this:

let json: JSON? = myModel.toJSON()

becomes:

let json: JSON? = myModel.toEncodableJSON()

NOTE: Similar usage applies to arrays, with from(decodableJSONArray:) and toEncodableJSONArray() respectively.

Step 3: Update your model to use Codable for decoding/encoding

This means fleshing out that init(from decoder: Swift.Decoder) throws or func encode(to encoder: Swift.Encoder) throws method. Or, better yet, removing them if Codable can synthesize your decoding/encoding for you.

Rinse and repeat this process for every Gloss model. You can leave the fallback Gloss methods in place until you're comfortable with your Codable implementation - then take the Gloss wheels off and ride into the sunset with Codable 🌅

Backwards Compatibility

If you are receiving errors anytime along the way don't worry - these changes are backwards compatible. What the new from(decodableJSON:) and toEncodableJSON() methods do under the hood is attempt to use Codable, but fallback to Gloss if any errors occur. They're also nice enough to log errors to the console to help you figure out where your migration is going astray.

If your Codable definitions are sound, but you're still receiving errors - you may need to explicitly configure a JSONDecoder or JSONEncoder and pass them along. A common reason for this is if your JSON is in snake_case, whereas Codable defaults to camelCase.

let mySharedJSONDecoder: JSONDecoder = ...
let myModel: MyModel? = .from(decodableJSON: someJSON, jsonDecoder: mySharedJSONDecoder)
let mySharedJSONEncoder: JSONEncoder = ...
let json: JSON? = myModel.toEncodableJSON(jsonEncoder: mySharedJSONEncoder)

⚠️ One Caveat: Nested Data ⚠️

One significant caveat is for models using a special feature of Gloss that allows nested values to be retrieved using a period-delimited string.

Before migrating to Codable, it may be simpler to un-nest those values by creating the nested models you were avoiding in the first place 😰 It's what Codable encourages regardless! Alternatively, you can use Codable's nested containers syntax.

Case Study

Let's look at how we'd migrate one of the models found in the Demo project. We start with a simple model of a GitHub Repo owner. Here's our JSON:

{
    "id": 123,
    "html_url": "https://github.com/someUser"
}

Preparing for Migration

First, we'll add conformance to Codable and update our call-sites.

Decodable

Our original RepoOwner model looks like this:

import Gloss

struct RepoOwner: JSONDecodable, JSONEncodable {

    let ownerId: Int
    let url: String?
    
    init?(json: JSON) {
        guard let ownerId: Int = "id" <~~ json else { return nil }
        self.ownerId = ownerId
        self.url = "html_url" <~~ json
    }
    
    // ...
}

To start migrating to Codable, let's add conformance to Decodable.

extension RepoOwner: Decodable { }

At our call-sites, we currently create this model from JSON as such:

let json: JSON = ...
let repoOwner = RepoOwner(json: json)

Let's change that to use our new Codable-friendly method:

let json: JSON = ...
let repoOwner: RepoOwner? = .from(decodableJSON: json)

That's it! We're done preparing our Gloss decoding for a rewrite to Codable. For now, Gloss will attempt to use Codable but safely fallback to our Gloss decoding if there's an error.

Encodable

Let's look at the same steps for Encodable. Remember our model:

import Gloss

struct RepoOwner: JSONDecodable, JSONEncodable {

    let ownerId: Int
    let url: String?
    
    // ...
    

    func toJSON() -> JSON? {
        return jsonify([
            "id" ~~> self.ownerId,
            "html_url" ~~> self.url
        ])
    } 
}

To start migrating to Codable, let's add conformance to Encodable.

extension RepoOwner: Encodable { }

Currently, our call-sites look like this:

let someRepoOwner: RepoOwner = ...
let json: JSON? = someRepoOwner.toJSON()

Let's change it to use our new Codable-friendly method:

let someRepoOwner: RepoOwner = ...
let json: JSON? = someRepoOwner.toEncodableJSON()

That's it! We're done preparing our Gloss encoding for a rewrite to Codable. For now, Gloss will attempt to use Codable but safely fallback to our Gloss encoding if there's an error.

Codable Definitions

Now we're ready to write our actual Codable definitions. Let's update our Decodable extension:

extension RepoOwner: Decodable {

    fileprivate enum CodingKeys: String, CodingKey {
        case id, htmlUrl
    }
    
    init(from decoder: Swift.Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let ownerId = try container.decode(Int.self, forKey: .id)
        let url = try container.decodeIfPresent(String.self, forKey: .htmlUrl)
        self.init(ownerId: ownerId, url: url)
    }

}

and the Encodable one:

extension RepoOnwer: Encodable {

    func encode(to encoder: Swift.Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.ownerId, forKey: .id)
        try container.encode(self.url, forKey: .htmlUrl)
    }

}

Looks great! But, even though these Decodable and Encodable definitions are sound....we're still getting an error.

It turns out that Codable assumes JSON keys will be in camelCase, whereas our JSON uses snake_case. We simply have to create our own JSONDecoder and JSONEncoder configured as such and pass those along. The good news is they will still come in handy when we ultimately remove Gloss!

extension JSONDecoder {
    
    static func snakeCase() -> JSONDecoder {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }
    
}


extension JSONEncoder {
    
    static func snakeCase() -> JSONEncoder {
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        return encoder
    }
    
}

And our call-sites now become:

let repoOwner: RepoOwner? = .from(decodableJSON: json, jsonDecoder: .snakeCase())

and:

let json: JSON = someRepoOwner.toEncodableJSON(jsonEncoder: .snakeCase())

There we go! Our Codable code-paths now should be executing and our Gloss defintion is defunct ✨

Embracing Codable

Could we do better? Could we get Swift-ier?

The answer is: Yes. One of the most powerful things about Codable is it it can auto-magically use property names to synthesize our JSON decoding and encoding for us. Unless we have a special need for our key names or de/encoding logic, this is what we should aim for.

Let's revisit our JSON:

{
    "id": 123,
    "html_url": "https://github.com/someUser"
}

If we match our RepoOwner property names to id and html_url respectively, Codable will take care of the rest. Our penultimate model defintion looks like this, with Codable doing the work but our Gloss fallback in place:

import Gloss

struct RepoOwner: JSONDecodable, JSONEncodable {

    let id: Int
    let htmlUrl: String?
    
    init?(json: JSON) {
        guard let id: Int = "id" <~~ json else { return nil }
        self.id = id
        self.htmlUrl = "html_url" <~~ json
    }
    
    func toJSON() -> JSON? {
        return jsonify([
            "id" ~~> self.id,
            "html_url" ~~> self.htmlUrl
        ])
    }
}

extension RepoOwner: Codable { } // Codable covers both Decodable & Encodable

Using Data Instead of JSON to Create Models

In the places where you've come to rely on Gloss's JSON type, you'll eventually need to pass Data, as that is what Codable uses. To get a jump using decode(:), one option is use the same method Gloss uses to do Data transformation:

import Gloss

let mySharedSerializer: GlossJSONSErializer = ...
let json: JSON = ...
if let data: Data? = mySharedSerializer.data(from: json, options: nil) {
    let myModel: MyModel? = try? myJSONDecoder.decode(MyModel.self, from : data)
    ...
}

Finally

The last step, once we're comfortable with our Codable integration, is to strip Gloss away. At the end of the day, our beautiful model looks like this:

struct RepoOwner: Codable {
    let id: Int
    let htmlUrl: String?
}

And our need for snake-case is defined in just one place, instead of stringly-typed in each and every model. Talk about lightweight!

Take the opportunity with this migration to pare your models down to the slim amount of code Codable needs to work its magic and detangle your networking code from the details of JSON serialization. Future you will be grateful! 🔮

Errata

If you find an issue with this guide or would like to add helpful content given your own migration, please submit a Pull Request.