This workshop will be retired on May 31, 2020.
Encoding and Decoding17:35 with Pasan Premaratne
One of the biggest complaints about Swift was the lack of native serialization and archival APIs, particularly for value types that couldn't bridge to Objective-C. Swift 4 introduces a comprehensive solution that is easy to use and flexible enough for most needs.
Everyone's favorite change to Swift 4 is, of course, the latest way to parse JSON. 0:00 If you've been part of the Swift community for a while now, you'll know 0:06 that every month we have a new hot way to parse JSON, or so it feels like. 0:10 Well, now finally, we have an official way. 0:15 So let's take a look. 0:18 The Swift evolution proposal for this addition is SEO166 and 0:20 is titles Swift Archival and Serialization. 0:26 Let's talk about the why first. 0:30 Swift didn't ship with any native archival or serialization APIs. 0:33 And instead benefited from the underlying Objective C implementations 0:38 available to it. 0:43 While this allowed us to achieve certain end goals, it was both restrictive, 0:44 in that only ns object sub classes could benefit, and 0:48 we had to implement solutions for value types. 0:52 And as always the existing solutions, 0:54 nsJSON serialization and so on, are far from Swift like. 0:57 The goals therefore, for this proposal, were to allow for native, archival, 1:02 and serialization of Swift enums and structs and to do so in a type safe way. 1:08 Let's introduce some sample JSON to our playground, so 1:13 we can see how this works in practice. 1:17 As an added bonus, we get to use Swift 4 new multi-line string literal syntax. 1:19 Let say let rawJson equal one, two, three, 1:25 we'll open a set of braces here, and then close this string literal. 1:29 And then here it will say name is the first key and 1:36 the value is The Lion King. 1:43 We can now convert this rawJson to a data type, 1:47 so say let json = rawJson.data using utf8 1:53 string encoding, and we'll unwrap this. 1:59 Okay, now let's define a type and we'll keep it simple here so 2:05 we can see how the new API works. 2:08 We'll build up from here. 2:10 So struct Movie, it's good to have a name. 2:11 We want to decode the JSON we have into an instance of this struct Movie. 2:17 And doing that in Swift 4 is super easy. 2:23 First, we'll make our model conform to a new protocol Codable. 2:27 After that all we need to do is say let lionKing, 2:34 this is the constant we're assigning the value to. 2:37 Let say try and 2:40 we'll force this because we don't want to worry about error handling here. 2:40 JSONDecoder()I.decode(Movie.self, from: json). 2:44 We have an error here, and 2:57 that's because we don't need this exclamation right there. 2:59 And that's all there is to it. 3:05 If we inspect the values on the instance, you can see that they match the JSON data. 3:07 So we can see lionKing.name, and in the results area, 3:11 you'll see the name of the movie. 3:15 Pretty easy. 3:18 So how does this work? 3:19 The protocol we conform to, Codable, 3:21 is actually two underlying protocols in Codable and decodable. 3:24 Command click and Jump to Definition and you'll see that in 3:29 Codable defines a type that can encode itself into an external 3:34 representation by providing an implementation for an encode function. 3:40 So here is see, public func encode. 3:46 Decodable, the counter part, defines a type that can decode itself 3:49 from an external representation, which is what we did with our code. 3:54 The protocol does this through an init from Decoder initializer. 3:59 Now if you're familiar with Objective C this looks a lot like NS coding. 4:04 The added bonus here is that since this is Swift, 4:08 through protocol extensions we already get default 4:12 implementations for both init from Decoder and encode to. 4:17 We already saw this in action, since adding Codable conformance 4:22 to our type back here, involved no additional work on our end. 4:27 So how does this default implementation work. 4:32 Structure types like ours which define a set of properties, 4:35 encode and decode these properties using a set of keys. 4:39 A single key corresponds to a single property, and for the Codable protocol, 4:43 these keys need to conform to a third protocol named CodingKey. 4:49 If we navigate to the Codable protocol and then look around, 4:54 you'll see there's a public protocol CodingKey. 4:58 So this is a short protocol here. 5:02 A key returns a string value, and optionally, 5:07 an integer value if we use integers as keys. 5:11 A key can also be initialized by using a string or an int. 5:16 Think of these as enums with raw values. 5:21 If the raw value is a string, then you can get the string by calling string value. 5:24 And similarly, you can initialize the enum with the string value initializer. 5:30 And in practice, this is exactly how it works. 5:35 By default, 5:38 the compiler generates an enum named CodingKeys that confirms to the protocol. 5:39 And provides a mapping of property name to string value. 5:45 Now this obviously confusing. 5:48 You might not be able to follow along by just hearing me talk. 5:50 So let's provide our own implementation for this. 5:52 So let's have it then. 5:55 All right, so inside the movie type, we'll add a new enum. 5:57 We'll name this CodingKeys, and it's going to have strings as raw values and 6:02 it's going to conform to the CodingKey protocol. 6:09 We'll give this enum a single value, case name. 6:14 Okay, so here we've defined an enum named CodingKeys that defines a single member 6:20 named name, and this value matches up with the property we've defined in Movie. 6:26 Note that CodingKeys must be defined as nested enum within the type we're 6:31 providing information for. 6:36 Now we've also specified that CodingKeys is a raw value of type string. 6:38 By default, this means the case name is used as a string raw value, 6:43 which is then used to satisfy the CodingKey protocol. 6:48 When we use a decoder, it asks the type for a set of keys that map to properties. 6:52 So in our case, 6:59 we have a JSONDecoder which asks the movie type for a set of keys. 7:00 Now we won't get into the mechanics of how exactly that occurs over here. 7:06 Using the keys we've defined in the CodingKeys enum. 7:10 The decoder can parse the JSON extract the relevant values. 7:14 So what does that mean? 7:18 So in this case the JSONDecoder asks for a set of keys and 7:19 in here in this example it gets a single key back. 7:23 The enum value CodingKeys.name. 7:27 It then calls the string value property on this key to get the raw value and it 7:30 expects this string value to correspond to a key in the JSON schema, which it does. 7:36 So here the string value will get back as name, 7:42 which is our raw value that we've defined here. 7:45 And then, JSONDecoder is going to look inside this JSON and see, hey, 7:48 is there a key named name? 7:52 And yes, there is. 7:54 Using this key it then gets a value, and since inside CodingKeys, 7:56 this value name corresponds to this name property, 8:01 it can now assign this value over to the name property. 8:05 So let's take this step further, let's add one more property to our type. 8:09 Here we will say let releaseDate, and this is of type string. 8:15 The playground should immediately raise an error, because we haven't 8:22 defined a matching entry in the CodingKeys enum, so everything starts to fail. 8:26 And you can see here that it says type movie does not conform to Decodable. 8:30 So let's add this in. 8:35 We need now, to define a value inside CodingKeys, 8:35 that matches this property exactly. 8:39 So here, we'll say case releaseDate. 8:41 Now when we do that, again, we'll get an error. 8:46 We haven't talked about error handling yet, but here we have an error, 8:49 because now, JSONDecoder, when asked for keys, it gets two keys back. 8:53 So the first one name gives a raw value of type name or a raw value of name, and 8:57 it then checks to see, hey, in this JSON is there something with the key, name? 9:03 And yes, we do have that. 9:08 We've been through that already. 9:10 However, for the second one, it is going to get a releaseDate and then it is going 9:11 to look back in the JSON and see, hey, is there value for the key releaseDate? 9:15 Now because there isn't one it fails. 9:20 And you can see this is console log, there is a bunch of information provided. 9:23 And honestly they detailed errors that you get from using Swift's implementation 9:27 of Codable and JSONDecoder and 9:32 all of that is one of my favorite parts of the new decoding APIs. 9:34 So here, it says that under debug description it says, 9:38 no value associated with the key releaseDate. 9:42 Now one way we can get rid of this is by marking the property as optional. 9:45 So if I mark the releaseDate property as optional, everything should work again. 9:50 Now the reason this happens is that the decoder is ignoring the missing data now. 9:55 Because it knows that the type's contract can be satisfied without it, 10:01 releaseDate can be set to nil. 10:04 Instead of doing that however, let's add a releaseDate key value pair to our rawJson. 10:07 In Swift, the standard for constant variable or 10:14 property naming is lower camel case. 10:16 But in JSON, it's pretty frequent you will encounter snake case. 10:19 So in here, I was saying, 10:23 release_date and the value here is 10:28 2014-01-12T14:00:00Z. 10:35 Now this is a very specific date format. 10:45 We'll talk about this in a second. 10:48 Okay, now we've added a releaseDate value to the JSON so it should work, right? 10:50 No, this does nothing to make the error go away. 10:58 And the reason now, is because the raw value string generated by 11:01 the coding key enum, doesn't match the key in the JSON schema. 11:05 Remember that this raw value that we get from this enum value releaseDate 11:10 is used by the decoder to map the property to the key value pair in JSON. 11:15 So here it says or 11:20 it thinks that its property name is releaseDate which is lower camel case. 11:21 It looks for a key with the same values, so releaseDate in lower camel case. 11:26 It doesn't find it because this is in snake case and it fails. 11:31 One way we can solve this specific error is to define your properties and 11:36 CodingKey values to match exactly what the JSON spec defines. 11:41 So in this case we could rename our property to release_date. 11:46 And now if we change this as well. 11:50 It should work, except I think I typed this wrong. 12:01 So let me double check. 12:05 release_date: 2014, 12:08 this should be 12. 12:13 There we go. 12:16 And we have some unnecessary space I believe. 12:20 And we need a comma after name. 12:23 Look at that, I'm terrible at writing JSON. 12:26 Okay, so now everything works. 12:28 And if we are to do lionKing.release_date we should have a value. 12:31 Unfortunately this is terribly unSwift like. 12:37 So we don't want to do snake case here because that's not the convention in 12:41 Swift. 12:45 Now remember the way the decoder works is by mapping the string value of the enum 12:46 cased to the key value pair. 12:51 So rather than modify the property name instead we can provide a custom string 12:52 value for the release date case. 12:57 So I'm gonna change this back to our normal convention and 12:59 here I'll change this as well. 13:03 Remember the mapping here is simply the raw value of this enum 13:05 member over to the key name in JSON. 13:09 So rather than using the default raw value, 13:12 we can say, release_date should be the raw value. 13:15 And now we have a perfect mapping and this should work. 13:18 There we go. 13:23 So now JSONDecoder is going to ask for a key that corresponds releaseDate. 13:24 It gets this enum value back. 13:30 It calls raw value, or the string value on it, it gets this value back, 13:33 and that is the value it uses to check in the JSON. 13:38 In this way, we can ensure our proprieties are initialized, 13:42 even when the keys don't match up. 13:46 Now the type of release date property is string, but 13:49 ideally we'd like to change that to date. 13:52 Built in encoders and decoders like JSONDecoder have support for dates, 13:56 URLs, and other Swift types right out of the box. 14:00 So let's change releaseDate, instead of a string to be a date. 14:04 Now all we have to do to get the date value out of the JSON and convert it to 14:09 the right value for a struct is to inform the decoder of the date format it is in. 14:14 So over here we have the decoder, let's grab that out and assign it to a constant. 14:20 So we'll say, let decoder equal and we can now use a constant in here. 14:27 So decoder.decode and we'll tell the decoder 14:32 that the date decoding strategy Is iso8601, 14:37 which is the date format that I used earlier when writing the date. 14:42 And now everything works again. 14:49 So if I were to type out lionKing.releaseDate, 14:51 now we have a nicely formatted date object. 14:56 Let's look at one more feature here. 15:00 What about nested types? 15:02 So let's add an actor type to our file right about movie. 15:03 Say struct Actor conforms to 15:07 Codable as a name of type String. 15:12 Back in Movie, let's also add a new stored property to the movie type. 15:17 So let actors which is an array of actor. 15:21 Now everything will fail again, so we need to add a matching enum key. 15:34 So case actress. 15:38 And let's modify our JSON as well. 15:41 So add a comma. 15:45 Actors, and this is a nested array, The first one. 15:48 We'll give it a name. 15:59 And for the second one. 16:10 Name. 16:15 Okay, let's format this a bit better. 16:20 All right, I think that should be it, yeah, and then we need to close the array. 16:28 Okay, and the second I do this all of this should work automatically. 16:36 Query the property and you'll find the right values in there. 16:39 So I can say, lionKing.actors.first!.name and 16:42 James Earl Jones should show up right there. 16:49 With nested data, the easiest way to decode information is to ensure any 16:55 wrapper structure is also represented through the individual types. 17:00 For example, with the code we've written so far, 17:04 we won't be able to just extract the actor's information from within 17:07 the movie object without having a valid movie type. 17:11 This might be a pain depending on how you're trying to parse the information. 17:15 But there's a lot more you can do with the new APIs, but 17:19 this should be enough to give you an idea. 17:22 As always, check the teachers notes for more references and 17:25 resources that touch on both encoding and decoding in more detail so 17:28 that you can dig into it further. 17:32
You need to sign up for Treehouse in order to download course files.Sign up