
This video is only available to subscribers. Start a subscription today to get access to this and 470 other videos.
Swift 4 JSON Parsing
Links
- Ultimate Guide to JSON Parsing with Swift 4 - I ended up writing a massive post on this topic for my blog, since I it will be incredibly useful to have as a reference for developers.
(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.