- JSON everywhere
- How would Apple do it?
- Problems with Apple's approach
- Using enum to store all the strings
- Using
SwiftyJSONto remove boilerplate SwiftyJSONModelfor the rescue- Type-safe and autocompleted keys for the
JSON - Return Types are inferred
- Verbose errors
- Easy access to nested
JSON - Conclusions
Every app now heavily relies on transferring data through internet. No need to explain it 😉.
And, of course, the most popular format is JSON. In Swift and Objective-C
we need to parse JSON first then map it to native objects and then work with it.
Let's be more specific and consider the following example:
{
"firstName": "Oleksii",
"lastName": "Dykan",
"age": 24,
"isMarried": false,
"height": 170.0,
"hobbies": ["bouldering", "guitar", "swift:)"]
}Imagine, that the back-end you're working with provides you the JSON above. This is the representation of the Person model
and has just standard fields. In our app we would like to view the details of that person and we would like to map it to the
following model:
struct Person {
let firstName: String
let lastName: String
let age: Int
let isMarried: Bool
let height: Double
let hobbies: [String]
}Please note! I chose struct here, but the example can be applied to any type such as class, enum etc.
So let's try to make it work!
The best way to solve iOS-related problem is to look for the solution in Apple's documentation. We would find a really nice article on swift's blog that is called exactly how we want: Working with JSON in Swift
To make long story short, Apple reccomends to use Foundation's framework JSONSerialization to convert Data into swift's native objects. In our case it would look like this:
let data: Data // received from a network request, for example
let json = try? JSONSerialization.jsonObject(with: data, options: [])let json in our case is of Type Any? and in order to work with it we would have to cast it to [String: Any] and then extract values.
If we read Apple's article further, we will see, that the actual mapping Apple suggests to do the following way:
extension Person {
init?(json: [String: Any]) {
guard let firstName = json["firstName"] as? String,
let lastName = json["lastName"] as? String,
let age = json["age"] as? Int,
let isMarried = json["isMarried"] as? Bool,
let height = json["height"] as? Double,
let hobbies = json["hobbies"] as? [String]
else {
return nil
}
self.firstName = firstName
self.lastName = lastName
self.age = age
self.isMarried = isMarried
self.height = height
self.hobbies = hobbies
}
}
let data: Data // received from a network request, for example
let json = try? JSONSerialization.jsonObject(with: data, options: [])
if let json = json as? [String: Any] {
let person = Person(json: json)
print(person)
}Here we create an extension to our Person model and add an initializer that takes as an argument a
Dictionary that was provided to us by JSONSerialization framework.
Then we exctract all the properties from json Dictionary and cast them to Types that we expect, like here:
let firstName = json["firstName"] as? StringIf casting fails at some point, the whole initialization fails and we return nil.
Then we assign all the values to the properties in our model, like:
self.firstName = firstNameAnd that's it! We are ready to use our Person model in our App.
But is this the best way?🤔
Althought it seems quite straightforward and easy, current approach has several improtant drawbacks:
- All the keys are just raw strings. This means, that it is really easy to make a typo and never notice it as swift's compiler cannot help us with strings
- A lot of boilerplate. Over and over again we have to cast to
String,Intand[String]and then assign the variables to our proprties in model. Really annoying 😤. - We never know where exactly the error happened. Our initializer is
Optionaland if something fails, we will just receivenil. But in order to understand what exactly when wrong, we will have to debug the json, go through all the keys manually and see what is missing or what has different type. Can't it all be automated?
Can we handle all these cases?
The first problem we would like to solve, is to remove the raw strings. Swift has a very nice feature as enum with RawValue. So we will keep all the key strings in the separate enum like this:
enum PropertyKey: String {
case firstName, lastName, age, isMarried, height, hobbies
}Every case in this enum is backed by a raw string. So now in order to get the string from enum case we have to acess it's rawValue:
print(PropertyKey.firstName.rawValue) // prints "firstName"So now let's apply this approach and use it in our model:
extension Person {
enum PropertyKey: String {
case firstName, lastName, age, isMarried, height, hobbies
}
init?(json: [String: Any]) {
guard let firstName = json[PropertyKey.firstName.rawValue] as? String,
let lastName = json[PropertyKey.lastName.rawValue] as? String,
let age = json[PropertyKey.age.rawValue] as? Int,
let isMarried = json[PropertyKey.isMarried.rawValue] as? Bool,
let height = json[PropertyKey.height.rawValue] as? Double,
let hobbies = json[PropertyKey.hobbies.rawValue] as? [String]
else {
return nil
}
self.firstName = firstName
self.lastName = lastName
self.age = age
self.isMarried = isMarried
self.height = height
self.hobbies = hobbies
}
}
let data: Data // received from a network request, for example
let json = try? JSONSerialization.jsonObject(with: data, options: [])
if let json = json as? [String: Any] {
let person = Person(json: json)
print(person)
}So now we don't have any raw strings anymore, which is good.
However, we still have several problems:
- We introduced even more boilderplate. Now we have to write
PropertyKey.*enumCase*.rawValue - It is still possible to use raw strings. Noone restricts us from using string instead of
enum'scase. So we the compiler can help only partially
Using SwiftyJSON to remove boilerplate
As we are good guys and we know what Open Source is, we will soon find out SwiftyJSON.
SwiftyJSON introduces a special type JSON instead of type-erased Any that we receive as a result of JSONSerialization. So in our case we will create json as the following:
import SwiftyJSON
let data: Data // received from a network request, for example
let json = JSON(data: data) // creates type JSON from Data
print(json)Seems pretty straightforward. Now we don't need to cast to dictionary as we did before, and we can use JSON type directly in the initializer. So our Person model now becomes the following:
import SwiftyJSON
extension Person {
enum PropertyKey: String {
case firstName, lastName, age, isMarried, height, hobbies
}
init(json: JSON) {
firstName = json[PropertyKey.firstName.rawValue].stringValue
lastName = json[PropertyKey.lastName.rawValue].stringValue
age = json[PropertyKey.age.rawValue].intValue
isMarried = json[PropertyKey.isMarried.rawValue].boolValue
height = json[PropertyKey.height.rawValue].doubleValue
hobbies = json[PropertyKey.height.rawValue].arrayValue.map({ $0.stringValue })
}
}
let data: Data // received from a network request, for example
let json = JSON(data: data) // creates type JSON from Data
let person = Person(json: json)
print(person)This looks much nicer as we don't have annoying castings anymore and we use convenient methods that JSON has to get String, Int, Bool etc.
We removed quite a lot of boilerplate code, but, nevertheless, we still have quite a lot problems with the chosen approach:
stringValue,intValue,boolValuegive us non-optional values. That means that we will never know that something went wrong with our json. For example, if in our json value for keyfirstNamewill be absent or wrong (for example there will beIntinstead ofString) we will never be notified and in our casefirstNamewill be just an empty string (like"")- Still a lot of boilerplate with specifying the type of value. We still have to explicitely state
stringValue,intValueetc. which is somewhat less boilerplate than we had before withOptional casting, but is still quite annoying. - Even more boilerplate with arrays.
arrayValueproperty ofJSONgives usArrayofJSON([JSON]) so we need to manually map over it and getstringValuefrom each element.
SwiftyJSONModel for the rescue
With all the problems in mind, I decided to write a microframework on top of the SwiftyJSON. Let's take a look on how would the same model look like when using SwiftyJSONModel and then we'll discuss all the features that it introduced.
So now our Person model looks like this:
extension Person: JSONObjectInitializable {
enum PropertyKey: String {
case firstName, lastName, age, isMarried, height, hobbies
}
init(object: JSONObject<PropertyKey>) throws {
firstName = try object.value(for: .firstName)
lastName = try object.value(for: .lastName)
age = try object.value(for: .age)
isMarried = try object.value(for: .isMarried)
height = try object.value(for: .height)
hobbies = try object.value(for: .hobbies)
}
}
let data: Data // received from a network request, for example
let json = JSON(data: data) // creates type JSON from Data
do {
let person = try Person(json: json)
print(person)
} catch let error {
print(error)
}Looks pretty easy. Now let's dive into details of what we actually gained with this approach
As you might notice, now instead of using JSON Type from SwiftyJSON we now use a wrapper Type on top of JSON which is called JSONObject. As you can also see, JSONObject takes a generic Type JSONObject<PropertyKey>. This actually tells JSONObject which enum do we use to store our keys.
So what we gain:
- Now
JSONObjectlimits the keys only to the enum that we specified. This in turn introduces autocompletion feature for our keys:
- Removed boilerplate code. So now the compiler knows what enum we use, so there is no need to do
PropertyKey.hobbies.rawValuenow we can directly use:.hobbiesand that's it. - Keys are now Type-Safe. That means that we no longer can use random raw strings as keys for our
JSON. We are restricted to ourPropertyKey enumand the compiler will give us compile-time error when we try to use invalid keys.
Looks nice! And it's just the beginning!
Apart from the type-safe keys, we no longer have to write stringValue, intValue etc. The frameworks knows which types should it return as when you did:
let firstName: Stringyou already specified that firstName is String.
So now instead of:
firstName = json[PropertyKey.firstName.rawValue].stringValueThere is no need to specify stringValue and now you can just write the following:
firstName = try object.value(for: .firstName)This removes quite a lot of annoying boilerplate that we had when we did the casting with Apple's approach and when we used SwiftyJSON alone as well. But that's not all.
Now for the arrays we don't need to map explicitly and convert to specific type.
So instead of:
hobbies = json[PropertyKey.height.rawValue].arrayValue.map({ $0.stringValue })Now we do just:
hobbies = try object.value(for: .hobbies)It works the same as with regular String. The compiler already knows that you expect an Array of Strings so there is no need to do it again yourself.
Consider the following JSON:
{
"firstName": "John",
"lastName": false,
"age": 24,
"isMarried": false,
"height": 170.0,
"hobbies": ["bouldering", "guitar", "swift:)"]
}Here we have an invalid value for key lastName as we expect it to be String, but instead we receive Bool. Before, there was no way for us developers to understand what exactly went wrong with the JSON and we had to debug quite a lot in order to understand what caused the problem.
However, now SwiftyJSONModel tells use which property exactly was invalid:
let data: Data // received from a network request, for example
let json = JSON(data: data) // creates type JSON from Data
do {
let person = try Person(json: json)
print(person)
} catch let error {
print(error) // prints: [lastName]: Invalid element
}As you can see, we now immediately understand what property was invalid in JSON and we can talk to our back-end developers or adjust our model respectively.
Consider the following JSON
{
"city": "NY",
"country": {
"name": "USA",
"continent": {
"name": "North America"
}
}
}So we have a nested JSON here that goes 2 levels deep. However, we do not want to create separate Model for each nested object and we want just to map to the following Model:
struct Address {
let city: String
let country: String
let continent: String
}our microframework allows to do it quite easy:
extension Address: JSONObjectInitializable {
enum PropertyKey: String {
case city, country, continent
case name
}
init(object: JSONObject<PropertyKey>) throws {
city = try object.value(for: .city)
country = try object.value(for: .country, .name)
continent = try object.value(for: .country, .continent, .name)
}
}Here we can acess the object by the full keypath to it:
.country, .continent, .nameAnd in case of error, we will receive the following:
[country][continent][name]: Invalid element
So let's recall all the things we gained from using SwiftyJSONModel:
- Keys for the
JSONare now Type-safe - Removed all the boilerplate code
- Have better error handling system
- Easy to access nested
JSON
I really look forward to your feedback and of course, don't forget to fork me on github 😉

It is not needed now as we have Codable, Decodable protocol.