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
Upgrade Gloss to minimum version 3.2.0
Version 3.2.0
adds methods to help translate between Gloss and Codable models.
The following is a summary of steps to take when migrating any single model from using Gloss to using Codable.
Gloss | Codable |
---|---|
JSONDecodable |
Decodable |
JSONEncodable |
Encodable |
Glossy |
Codable |
Gloss.Decoder |
Swift.Decoder |
Gloss.Encoder |
Swift.Encoder |
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
.
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.
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 🌅
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 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.
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"
}
First, we'll add conformance to Codable and update our call-sites.
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.
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.
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 ✨
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
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)
...
}
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! 🔮
If you find an issue with this guide or would like to add helpful content given your own migration, please submit a Pull Request.