Swift 4 JSON Parsing

Episode #278 | 17 minutes | published on June 22, 2017 | Uses Xcode-9.0-beta1, swift-4
Subscribers Only
Swift 4 finally answers the long-debated question of: How should I parse JSON with Swift? In this episode we'll take a look at the new Codable protocol in Swift 4, and talk about how to use JSONEncoder and JSONDecoder to serialize your objects into JSON and back again.

Links

(upbeat music)

Okay, let's take a look

at something new in Swift 4.

Before I start I wanna mention that I'm using

an early snapshot of Swift 4 and I'm also using

Xcode 9 beta 1 and so things may change slightly

if you're watching this video a little bit later,

but I'll include links to the exact versions

that I'm using so you can follow along if you want to.

So we're gonna answer the age old question

of, how do you parse JSON and Swift?

There's about 25 different libraries, all of them

have their own strengths and weaknesses

and Apple previously had guidance on this

which was more of a manual approach

and ultimately it boils down to the problem

that Swift's strongly type world

doesn't necessarily work well with the unstructured,

less strongly typed world of JSON.

There's less types and there's less rigidity

and so transferring between the two is difficult.

So we're gonna look at Swift 4's new features

in this vein and we're gonna start by creating a struct

that we can work with, we'll call it beer.

And beer will have a name, which will be a string

and will have an alcohol by volume which will be a float

and let's say a brewery which will be a string.

So that's what we wanna start with.

And I want to take an instance of this beer,

so let's create one, we will create one

called lawnmower which has alcohol by volume 4.9%

and the brewery is Saint Arnold.

And just make sure that that compiles and creates

a beer and it does.

So we wanna turn this into JSON.

So to do that we can use the new codable protocol.

Now notice that as I typed that the new codable

protocol is actually a decodable and encodable.

So this syntax here is Swift's way of saying

that this type is the union of those two other types.

Now previously you couldn't do this is Swift,

you could do this in Objective-C,

but basically as any type you can say this type

is actually a combination of these two other types.

So if we only want to decode something from JSON

into our object, then we can use the decodable protocol

and if we want to only encode something,

like take this beer and turn it into JSON

but never back again then we could use an encodable

protocol, but if we wanna do both then we use codable.

The benefit here is now I can create a JSON decoder

and I will create a new instance of that.

Then I'll call JSON decoder dot decode.

And I have to give it here a type and a data.

And actually I'm gonna start off the other way around,

I wanna encode this in JSON, so we're gonna use

a JSON encoder, I'll call that encoder.

And then we can say, encoder dot encode

and I paste in a value here that is encodable.

So that value here is going to be my beer.

And it is encodable because it adopts that codable protocol.

This call can throw exceptions, so we're gonna say,

try and that's gonna give me back a data.

So if all that works then our data

is going to have some data in it.

It has 55 bytes, we can see that here.

And so now we can just print this out.

So let me get a JSON string out of that.

So we'll initialize a string with that data

and we'll use the dot utf8 string encoding

and I'll just print that to the console below.

And you can see here that we have our string.

Let me force unwrap that so we get a nicer look at it.

There we go, there's our string.

It looks like a valid JSON document,

it has the keys exactly like our properties

were defined here, it looks really good.

But for the JSON encoder we want to tell it

that's pretty printed.

So we can tell it that it has some output formatting

and the outputting formatting types,

there's only two of them.

There's compact which is the default

which we already saw and there's pretty printed here.

And pretty printed will print it out

like you would typically see in API docs

or sample responses, things like that

that are a lot easier for us to see.

So I'm going to copy this and I'm going to use this

as another string so that we can do the reverse.

We wanna take that and turn it back

into another beer instance based on the string.

So let's call this input JSON.

I want to stuff this into a string,

but it spans multiple lines and it has these pesky

quote characters which I cannot swap these out

for single quotes, that's not exactly valid,

you're supposed to use double quotes here.

But because I'm using double quotes here

I would have to escape these, but turns out

that Swift 4 has this awesome new feature

called multi-line string literals.

And so I can basically, as long as I line this up properly,

I can move all of this over and then take my multi-line

string literal and start and end it

with these three quotes.

Now it has to be vertically lined up like I did here.

So you start it with three quotes,

that begins your multi-line string

and then the first non-white space line,

so it's actually stripping out, let's see if I can

show this, it's actually stripping all of that

out of the string.

So we don't have any leading white space

coming up to the first character

and as long as everything is lined up

with that first character the only leading white space

we're gonna have on subsequent lines

are the extra white space past that mark.

And so this is basically exactly what we want

when we're creating multi-line strings.

The other thing to notice is that we don't have to escape

the double quote here anymore because we're using

a different delimiter for our string.

So this is two thumbs up from me

because I do this type of stuff all the time

and it's really handy to not have to worry

about stripping leading white space.

And basically preserving whatever formatting you want.

So we've got an input JSON, I need to call,

convert that into data.

So we're going to grab that input JSON,

call data using encoding and pass in utf8.

And so now I've got my input data and now I can

create a decoder.

So we're going to create a new JSON decoder.

And then we're going to tell that decoder

that we're gonna decode a particular protocol.

It has to be something that implements decodable

and our beer type does, so we're gonna pass in

beer dot self as the object that we wanna get back

or the type that we wanna get back.

And then the data that we're going to be working with

is the input data and I'm gonna force unwrap it here

because I know it's gonna exist.

And again, this call can fail.

So I'm gonna use try bang and that's gonna give me back

my beer instance.

So I'm gonna call that beer2 and then we're just gonna print

the beer2's name property, or actually

we can use dump to dump all the properties of our beer.

And now you can see that we took this JSON

and we turned it into this object.

So it grabbed everything that we needed.

So far we basically haven't written any parsing code,

it's just given to us for free.

So I'm gonna talk about some of the differences here.

If we have a more complex type, let's say we have

a beer style that is a beer style enum.

And so here I'm going to have an enum for the beer style,

these are just going to be string value enums

and so we'll have a case for IPA,

we might have a case for lager,

we might have kolsch and there's many different beer types,

but I'll just leave a few for now.

So now it's complaining because beer is no longer codable.

By adopting this protocol the compiler will be okay

with it as long as every member of that type

is also codable.

And so string has a default implementation

of codable, so does float, so does this string

and many, many types like URLs and dates and everything else

they all have an implementation of codable,

but our beer style does not.

So since we're using a string backed enum

we can just say that this is codable also

and it will figure that out.

And so now our codable beer style will just give us

the key of style and the value of whatever

the raw value is on our enum.

So in order for us to create a beer

I need to tell it what style this is

and this is a kolsch style beer.

And so now when we print out our JSON here

you can see that it's got style kolsch

and that's exactly what we wanted.

But we're failing to parse this now

because the static string that I put here

doesn't include that value and we said that it's required.

We said that it's required based on our type

having no default value for this.

And so when we generate the codable implementation

it's expecting to see all of the keys

that we said that were required in the type.

So the other part to notice here is that this is raising

a Swift error that we can catch if we want to.

This is a key not found error, it tells us what key path,

or what they call a coding path,

so this is beer dot style and then the error context

gives us the coding path as well,

as well as the debug description.

Key not found while expect non-optional

beer style for coding key style.

So obviously this is invalid JSON

and if you're parsing arbitrary JSON

in a real application you probably wanna handle this

and fail gracefully.

So there are no fatal errors that's happening here,

this is all stuff that we can catch if we want to.

So you might provide a default value

or something else, but basically I am going to add

a comma there and just add in our style of kolsch.

Okay, we're back to parsing successfully.

So just to drive this point home a little bit more,

let's say we have another struct for brewery

and brewery has a name property which is a string.

We can make that codable and then we can change

our brewery to this particular type

and again, instead of creating the brewery like that

we'll create the brewery with a name.

And then we'll have to do the same thing here.

This brewery is now gonna be an object with a name.

And now we're back to parsing and now we've got

this nested object.

Now, how did it know that we wanted an object here?

It's the same that how it knew we wanted

an object here, the default implementation

creates what's called a keyed container.

So this is a keyed container and that is because

it has keys and values.

There are other things called unkeyed containers

which we can talk about later which are things

like arrays, like it's just a bunch of values

in an array or in a collection, it doesn't have any keys.

And so when you're building objects that are made up

of other objects you'll get basically what you expect

which is this nested style.

Okay, we've shown some examples

where all of these things are basically

the same name as what they show up in the API

and that's not really realistic.

Most APIs use Snake case for their API names.

So for instance if we had another field here

that was, I don't know, created at

we would expect to see something

that looks like this in the API,

but that's not good Swift style, that looks pretty ugly.

Let me finish this so we can get this compiling.

We will create a created at, and I'm gonna rename that

because I already think it's ugly.

It looks good in a language where that's the convention

like Ruby, but in Swift we don't expect that.

So we're gonna create a created at date

of today for our beer, but the API that it generates,

if you look at this JSON that it generates,

it generates that and that's not what we expect.

And this is probably also not what we expect

and we'll talk about that in just a second,

but how can I adjust this key to match

what the API would typically send us?

And so to do that we're going to specify

what the compiler has inferred for us.

So the compiler has inferred, and you'll probably see

down here we mentioned and glossed over

this coding path, so this is a coding keys type

that's attached to our beer and this has been given to us.

So we can even say beer dot coding keys

and it probably doesn't compile, it might be private.

But we can specify this ourselves.

So we can say that there's a coding keys enum

and these are strings and that this adopts

the coding key protocol.

So this is the basic structure that you would use

if you want to customize what the key values are.

So we now need a case for each one of these.

And this is enforced by the compiler

because we said we adopt the codable protocol,

that's going to expect a coding path

for each one of these instances.

So we can just have defaults for each one

that just matches the convention.

But for the one that doesn't match the convention,

created at, we can just assign that equal

to a different value here.

Okay, we are back to compiling.

And now you can see that our created at

JSON key is parsed exactly as we want it to.

It's encoded exactly like we would expect.

So now we just need to fix our example

static JSON to have that value.

So we're gonna say, created at and then we'll do colon.

Xcode is being a little bit annoying,

but there is that weird number in there

and we typically would want this

to be created, rather than having a floating point

number, which is probably the number

of seconds since the reference date or something,

to have ISO 8601 formatted date times.

And that is the common standard in most APIs

that you'll work with.

And so to do that we can go over here

to nsdateformatter.com, which if you weren't aware

is a site that I made.

This actually runs Swift on the server on Linux

and is hosted on Heroku and it gives you live examples

of all these different date format times

and you can play around with them here.

So for instance if we want to take this

which is ISO 8601 formatted time

and paste it into our format string

and we can play around with the date here

and it will live update based on

the response from the server.

So this is using actual NS Date Formatter on the server.

So we're gonna copy this ISO 8601 format string

and we're gonna go back here, actually I'm gonna

copy the date and we expect this to be parsed

properly, but we're getting a different error now.

This is giving us a Swift dot decoding error

dot type mismatch.

And it tells us the type that we're expecting

and then it gives us back that context.

The context tells us the coding path

as well as the debug description,

we expected to decode a double, but we found

a string slash data instead.

So we need to tell the JSON encoder that our date

should be formatted in that style.

So one way to get around this is to have two fields,

one of them is computed off the other,

but it turns out that the JSON encoder

supports this already.

So if we go to our encoder and we tell it

that we have a date encoding strategy

of one of these different strategies.

And one of them is ISO 8601.

So if we specify ISO 8601 it both creates it

in the right format and it should return it

in the right format and I think that it's not expecting

this plus 0000, so we might need to push

the Z at the end.

Let's see, what were we looking for?

We're still looking for a double,

that's because we need to tell our decoder

that we have the date decoding strategy of ISO 8601.

And now we have created this properly

and I was probably incorrect about

not being able to pass in the time zone offset there.

Now that should also work.

If you're not familiar with that,

this means no offset which means UTC time

and that is the same as passing in a Z,

which I think stands for Zulu, but I digress.

So basically we can now parse this object

and we can encode it in JSON and then decode it

back into our object.

And we did that with really minimal code.

The only thing we had to implement here

was this coding keys thing.

So there is more to cover here

so if your API is returning to something

a little bit more strange or just something

that doesn't follow these conventions,

there's basically a way to get essentially

any JSON document parsed, but how difficult

that will be will depend on how much it diverges

from quote, unquote standards

and how they've implemented this.

But I would say that JSON encoder and decoder

is a huge, welcome addition to the Swift language

and the fact that we can have these default

implementations of codable given to us

by the compiler is really awesome.

So I hope you found this episode useful

and we'll see you again next time.

blog comments powered by Disqus