JSON Parsing With Swift 4?
Swift 4 and Foundation has finally answered the question of how to parse JSON with Swift.
There has been a number of great libraries for this, but it is quite refreshing to see a fully-supported solution that is easy to adopt but also provides the customization you need to encode and decode complex scenarios.
It’s worth noting that everything discussed here applies to any Encoder
/Decoder
implementation, including PropertyListEncoder
, for instance. You can also create a custom implementations of these if you need something different like XML. The rest of this post will focus on JSON parsing because that is the most relevant to most iOS developers.
The easiest…
If your JSON structure and objects have similar structure, then your work is really easy.
Here’s an example JSON document for a song:
{
"name": "Deja que te toque",
"artist": "Rescate",
"released": "2004",
"country": "Argentina"
}
Our Swift data structure could look like this:
struct Song {
let name: String
let artist: String
let released: String
let country: String
}
To convert this JSON string to a Beer
instance, we’ll mark our type as Codable
.
Codable
is actually a union type consisting of Encodable & Decodable
, so if you only care about unidirectional conversion you can just adopt the appropriate protocol. This is a new feature of Swift 4.
Codable
comes with a default implementation, so for many cases you can just adopt this protocol and get useful default behavior for free.
struct Song : Codable {
// ...
}
Next we just need to create a decoder:
let jsonData = jsonString.data(encoding: .utf8)!
let decoder = JSONDecoder()
let song = try! decoder.decode(Song.self, for: jsonData)
And that’s it! We’ve parsed our JSON document into a beer instance. It didn’t require any customization since the key names and types matched each other.
Worth noting here is that we’re using try!
for the sake of an example, but in your app you should catch any errors and handle them intelligently. More on handling errors later on…
So in our contrived example things lined up perfectly. But what if the types don’t match up?
Customizing Key Names
It is often the case that API’s use snake-case for naming keys, and this style does not match the naming guidelines for Swift properties.
To customize this we need to peer into the default implementation of Codable
for a second.
Keys are handled automatically by a compiler-generated “CodingKeys
” enumeration. This enum conforms to CodingKey
, which defines how we can connect a property to a value in the encoded format.
To customize the keys we’ll have to write our own implementation of this. For the cases that diverge from the swift naming, we can provide a string value for the key:
struct Song: Codable {
// ...
enum CodingKeys : String, CodingKey {
case name
case artist = "band_name"
case released
case country
}
}
If we take our song instance and try to encode it as JSON, we can see this new format in action:
let encoder = JSONEncoder() let data = try! encoder.encode(beer) print(String(data: data, encoding: .utf8)!)
This output:
{"name":"Deja que te toque","band_name":"Rescate","released":"2004",
"country":"Argentina"}
The formatting here isn’t very human-friendly. We can customize the output formatting of the JSONEncoder
to make it a little nicer with the outputFormatting
property.
The default value is .compact
, which produces the output above. We can change it to .prettyPrinted
to get more readable output.
encoder.outputFormatting = .prettyPrinted
The new output looks like:
{
"name":"Deja que te toque",
"band_name":"Rescate",
"released":"2004",
"country":"Argentina"
}
JSONEncoder
and JSONDecoder
both have more options for customizing their behavior. One of the more common requirements is customizing how dates are parsed.
Handling Dates
JSON has no data type to represent dates, so these are serialized into some representation that the client and server have to agree on. Typically this is done with ISO 8601 date formatting and then serialized as a string.
Pro tip: nsdateformatter.com is a great place to snag the format string for various formats, including ISO 8601 format.
Other formats might be the number of seconds (or milliseconds) since a reference date, which would be serialized as a Number in the JSON document.
In the past we’d have to handle this ourselves, providing perhaps a string field on our data type and then using our own DateFormatter
instance to marshal dates from string values and vice-versa.
With the JSONEncoder
and JSONDecoder
this is all done for us. Check it out. By default, these will use .deferToDate
as the style for handling dates, which looks like this:
struct Foo : Encodable {
let date: Date
}
let foo = Foo(date: Date())
try! encoder.encode(foo){
"date" : 519751611.12542897
}
We can change this to .iso8601
formatting:
encoder.dateEncodingStrategy = .iso8601{
"date" : "2017-06-21T15:29:32Z"
}
The other JSON date encoding strategies available are:
.formatted(DateFormatter)
– for when you have a non-standard date format string you need to support. Supply your own date formatter instance..custom( (Date, Encoder) throws -> Void )
– for when you have something really custom, you can pass a block here that will encode the date into the provided encoder..millisecondsSince1970
and.secondsSince1970
, which aren’t very common in APIs. It is not really recommended to use a format like this as time zone information is completely absent from the encoded representation, which makes it easier for someone to make the wrong assumption.
Decoding dates have essentially the same options, but for .custom
it takes the shape of .custom( (Decoder) throws -> Date )
, so we are given a decoder and we are responsible for hydrating that into a date from whatever might be in the decoder.
Wrapper Keys
Often times APIs will include wrapper key names so that the top level JSON entity is always an object.
Something like this:
{ "songs": [ {...} ] }
To represent this in Swift, we can create a new type for this response:
struct SongList : Codable { let songs: [Song] }
That’s actually it! Since our key name matches up and Song
is already Codable
it just works.
Inheritance
Let’s say we have the following classes:
class Person : Codable
{
var name: String?
} class Employee : Person
{
var employeeID: String?
}
We get the Codable
conformance by inheriting from the Person
class, but what happens if we try to encode an instance of Employee
?
let employee = Employee()
employee.employeeID = "emp123"
employee.name = "Joe"
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(employee)
print(String(data: data, encoding: .utf8)!)
{ "name" : "Joe" }
Well that’s not what we wanted. As it turns out the auto-generated implementation doesn’t quite work with subclasses. So we’ll have to customize the encode/decode methods again.
class Person : Codable {
var name: String?
private enum CodingKeys : String, CodingKey {
case name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
}
}
We’ll do the same for the subclass:
class Employee : Person {
var employeeID: String?
private enum CodingKeys : String, CodingKey {
case employeeID = "emp_id"
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(employeeID, forKey: .employeeID)
}
}
This gives us:
{
"emp_id" : "emp123"
}
Well that’s not right either. We have to flow through to the super class implementation of encode(to:)
.
You might be tempted to just call super and pass in the encoder. This should work, but as of the current snapshot this causes an EXC_BAD_ACCESS
. I think this is a bug and will probably work in future snapshots.
If we did the above we’d get a merged set of attributes under the same container. However, the Swift team has this to say about re-using the same container for multiple types:
If a shared container is desired, it is still possible to call super.encode(to: encoder) and super.init(from: decoder), but we recommend the safer containerized option.
The reason is that the superclass could overwrite values we’ve set and we wouldn’t know about it.
Instead, we can use a special method to get a super-class ready encoder that already has a container attached to it:
try super.encode(to: container.superEncoder())
Which gives us:
{
"super" : {
"name" : "Joe"
},
"emp_id" : "emp123"
}
This produces the super-class encoding underneath this new key: ”super”
. We can customize this key name if we want:
enum CodingKeys : String, CodingKey {
case employeeID = "emp_id"
case person
}
override func encode(to encoder: Encoder) throws {
// ...
try super.encode(to:
container.superEncoder(forKey: .person))
}
Which results in:
{
"person" : {
"name" : "Joe"
},
"emp_id" : "emp123"
}
Having access to common structure in a superclass can simplify JSON parsing and reduce code duplication in some cases.
Further Reading
- Codable.swift — One of the great things about Swift being open source is we can just look at how these things are implemented. Definitely take a look!
- Using JSON with Custom Types — A sample playground from Apple that shows some more complex JSON parsing scenarios.
Conclusion
Ok, ladies and gentlemen, that’s it. I’ve tried to explain the latest Swift’s features to work with JSON, I hope that it to be useful in your daily work.
Thanks for reading us!