Apple announced Xcode 9 along with Swift 4 during the Keynote and State of the Union last week in San Jose for WWDC 2017. One of the most significant changes that made it into the Swift 4 Standard Library is a series of protocols for Encoding, Decoding and Serialization of type instances that allow users to convert to and from JSON, as well as to and from the local disk. These protocols, most notably Codable, Encodable and Decodable are intended to be the native Swift answer to several limitations that developers hit when dealing with serializing objects in Swift 3 or earlier.
These protocols have only been in the wild for a little over a week now, but Apple has some great documentation available online. I spent some time experimenting with these APIs in order to further understand how they work, what’s possible and some of the benefits and shortcomings that I might encounter using them in a production codebase.
Up until now one of the biggest open questions in the Swift community has been “which third party framework do you use to deserialize JSON? Or do you role your own?”. Since I first started writing Swift I’ve experimented with several frameworks and with rolling my own. A frustration that I always had with this problem was that each framework had its own unique personal opinion on the approach, often mistreating optionals, lacking strong error handling and making use of vague custom operators. Swift 4’s Codeable protocol offers a more universal and recommended approach to this problem, so I experimented with it based on a few key use cases that I aways have.
Consider you have a struct Product and you want to deserialize an instance from a JSON response received in a network request. Ensure that Product and any custom property types conform to the protocol Codable (or just Decodable if you’re not serializing back into JSON).
At the point where you want to deserialize a Data object into a Product model, initialize a JSONDecoder object and call decoder.decode(_: from:). This function will throw an error if the decoding operation fails, so you may want to wrap it in a do, try catch statement.
And that’s it. Assuming all of the property names on the Product model correspond exactly to the field names in the JSON structure you’re deserializing, Swift will take care of everything needed to initialize an instance of your model.
Serializing a Product back into JSON simply requires that the model and all its custom property types conform to the protocol Codable (or just Encodable if you’re not deserializing from JSON).
At the point where you want to serialize a Product instance into Data to be sent in a network request or anywhere else, initialize a JSONEncoder object and call encode(_:). This function will also throw an error if the encoding operation fails, feel free to wrap it up in a do, try, catch again.
Deserializing a Product model that has a property which is also a Codable type becomes very easy in Swift 4. Quite simply, ensuring that each nested type conforms to the Codable, (or justEncodable/Decodable where needed) is all that is needed. A JSONDecoder will handle decoding nested properties in the same way that it decodes a top level object, just as you would expect.
Custom Property Names
Of course, working with JSON is never that simple. As mobile engineers we more often than not don’t have control over the Network API that we’re consuming and want to define custom field names for the properties that we’re decoding from a JSON payload.
It’s important to understand that by default Swift is automatically using the property names you’ve defined as the field names to decode from the JSON. Defining custom field names for properties on a Codable type is as simple as defining an enum on your object called CodingKeys that has a rawValue of type String and conforms to the CodingKey protocol. You are required to define a case for each and every property on your model. RawValues for each case are then used as the JSON field name to be decoded from JSON.
Custom Key Paths
In the brief amount I’ve experimented with having more complex key paths and JSON structures, it seems that Swift’s approach to handling these takes some getting used to and involves falling back to a lot of boilerplate.
A common structure you may encounter is an object’s properties nested within a top level object, such as in this case a field called “product”. It’s important to understand that Swift’s approach to these nested key paths is by using the concept of a ‘Container’. Each object level within the JSON structure, is considered a Container within a hierarchy, and a Swift JSON Decoder will decode based on individual containers.
Since your Swift Codable type has a flat list of properties, by default the JSONDecoder will only attempt to decode your properties from the top level Container within the JSON payload. In order to achieve otherwise, you’ll need to write your own implementation of the public init(from decoder:) throws function and manually pull apart the nestedContainers that contain your properties.
In the sample above, a separate CodingKey enum is defined containing the field name for the top level container products. Within the Decodable initializer, the top level container’s values are deserialized using this key, and then each subsequent nested value in the nested container are deserialized one by one.
It’s clear that Swift’s handling of complex key paths and JSON structures relies on a lot of boilerplate and requires falling back to manually decoding each property like we’re used to. But in many ways this shortcoming encourages developers to work closely with API teams to define simpler JSON structures where possible.
One of the biggest shortcomings to many third party JSON deserializing/serializing frameworks was the lack of thorough error handling functionality. There are many, many different reasons why an object might fail the deserialization process such as a missing JSON field, an incorrect type, or a custom transformation failure.
Without any implementation of error handling it can be incredibly difficult to debug issues in production applications when something goes wrong. Getting to the root cause of these issues therefore involves cradling the Debug Console, setting countless Breakpoints and stepping through asynchronous code to figure out which fields are not being sent or are being sent incorrectly.
Swift’s JSONEncoder and JSONDecoder objects throw errors when decoding/encoding fail, and these errors provide clear and specific feedback to developers about exactly what went wrong.
debugDescription: "Key not found when expecting non-optional type String for coding key \"identifier\""))
If a non-optional property is not found within the JSON payload, a JSONDecoder will throw a DecodingError for the reason that the operation failed such as .dataCorrupted, keyNotFound, typeMismatch and valueNotFound.
These error cases are described further in Apple’s DecodingError documentation.
Decoding a field of a certain type from a JSON payload, and performing a transformation on this type into a different type is also a common operation performed when interacting with JSON.
Achieving this with Swift’s Codable protocol is as simple as defining your own implementation of the Decodable initializer as seen previously. Perform the decoding of your field and make your transformation before setting it as the property value within the initializer. In this case, the field creationDate is decoded as a String and stored in a constant. It is then transformed into a Date using a DateFormatter and set on the property creationDate as needed.
I’ve only had a very brief chance to experiment with and understand how Swift 4’s new Codable protocol works. I’ve stepped through a series of common use cases that I regularly encounter a daily basis. Most of the time Swift 4’s Coding APIs are easier to use, and far more concise than any of the third party frameworks that I’ve used before. I’m incredibly excited to make use of the Error Handling features that Apple have provided out of the box. I fully intend to extend upon this and hopefully create a system in which JSON Decoding errors are surfaced up to the UI in a debug build environment so that external API teams can debug issues with their APIs using the app, without any intervention from myself or any mobile developer on the project.
I’m also looking forward to putting these APIs more to the test on a production project, and hearing from the broader Swift community about their takeaways from these changes and how they effect apps of all different shapes and sizes.