My book on iOS interface design, Design Teardowns: Step-by-step iOS interface design walkthroughs is now available!
JSON and Swift
Many have tried to tackle the issue and there're now some really good solutions. Here are just a handful of them and there are 2 main categories.
Some solutions make it really easy to make sense of JSON data. This is significant because Swift's strong type-safety makes it hard to work with JSON without much explicit casting:
Others go a step further and help make it easy to work with actual data models. They help to decode JSON strings or objects into native types (read Structs
or Classes
):
After a brief survey of available options, it felt like something was missing. We didn't have a complete solution that made the round trip - one that is able to produce native types from JSON and also JSON from native types.
Part of the reason, I suspected was the difficulty of extending types of data models, of which value types (read Structs) are becoming increasingly popular. This, however, changed in Swift 2.0 with the ability to write extensions on protocols.
So here's my take on the problem - JSONCodable
.
It's by no means perfect - but I promise it will keep evolving as we discover better means of working with Swift.
Here's a quick run through of its features:
- It provides capabilities for encoding and decoding JSON
- It leverages features to expose an API as Swift-like as possible
- No operators were harmed in the making of this project
Still here? Let's take a look at how it works (you can also find this information on Github but I've tried to provide reasons for some of the implementation details here.)
JSONCodable
is made of two seperate protocols JSONEncodable
and JSONDecodable
.
JSONEncodable
generates Dictionarys
(compatible with NSJSONSerialization
) and Strings
from your types while JSONDecodable
creates structs (or classes) from compatible Dictionarys
(from an incoming network request for instance.)
Suppose we have the following data models:
struct User {
var id: Int = 0
var name: String = ""
var email: String?
var company: Company?
var friends: [User] = []
}
struct Company {
var name: String = ""
var address: String?
}
Encoding/Serialization
To encode JSON, we'll add conformance to JSONEncodable
. You may also add conformance to JSONCodable
.
extension User: JSONEncodable {
func JSONEncode() throws -> AnyObject {
var result: [String: AnyObject] = [:]
try result.archive(id, key: "id")
try result.archive(name, key: "full_name")
try result.archive(email, key: "email")
try result.archive(company, key: "company")
try result.archive(friends, key: "friends")
return result
}
}
extension Company: JSONEncodable {}
The default implementation of func JSONEncode()
inspects the properties of your type using reflection (see Company
.) If you need a different mapping, you can provide your own implementation (see User
.)
Using protocol extensions we can provide a default implementation that seems reasonable! That's great.
Decoding/Deserialization
To decode JSON, we'll add conformance to JSONDecodable
. You may also add conformance to JSONCodable
.
extension User: JSONDecodable {
mutating func JSONDecode(JSONDictionary: [String : AnyObject]) {
JSONDictionary.restore(&id, key: "id")
JSONDictionary.restore(&name, key: "full_name")
JSONDictionary.restore(&email, key: "email")
JSONDictionary.restore(&company, key: "company")
JSONDictionary.restore(&friends, key: "friends")
}
}
extension Company: JSONDecodable {
mutating func JSONDecode(JSONDictionary: [String : AnyObject]) {
JSONDictionary.restore(&name, key: "name")
JSONDictionary.restore(&address, key: "address")
}
}
Unlike in JSONEncodable
, you must provide the implementations for func JSONDecode()
. As before, you can use this to configure the mapping between keys in the Dictionary
to properties in your Structs
and Classes
.
We can't provide a default implementation of this because there's no way in Swift to dynamically set property values at runtime. The best we can do is use
inout
parameters (some have tried to obfuscate this using operators - Sigh)
Limitations
Your types must be initializable without any parameters, i.e. implement
init()
. You can do this by either providing a default value for all your properties or implementinit()
directly and configuring your properties at initialization.You must use
var
instead oflet
when declaring properties.
JSONDecodable
needs to be able to create new instances of your types and set their values thereafter. So we need to a standard way to initialize these types viainit()
and the properties must be mutable to allow setting their values.
What's next?
Ideally we'd allow for properties to be constant - which really might make sense for some situations. However, we then need some way of performing a complete initialization of the type (which introduces some ugliness in the API because consumers need to provide a non-trivial initializer.)
More symmetry in the API regarding errors when encoding and decoding JSON.
If you have any thoughts, let me know here or on twitter @matthewcheok
!