00:06 Last
week,
we played around with Codable
and wrote a custom decoder — not to decode
anything, but rather just for reflection. Today we'll continue looking into ways
to use Codable
, and we'll create an XML decoder.
00:23 For the Swift Talk backend, we connect with a subscription service,
Recurly, that uses a custom XML format. We have to parse various resources from
this service, like subscriptions and accounts, and all these resources use that
same format.
00:41 Generally, everyone who uses XML implements it in their own way, so
formats can look slightly different in how they structure certain value types or
in how they represent nil
. In today's example, we see that Recurly represents
a URL as an element with an href
attribute:
<?xml version="1.0" encoding="UTF-8"?>
<account href="https://domain.recurly.com/v2/accounts/06a5313b-7972-48a9-a0a9-3d7d741afe44">
<adjustments href="https://domain.recurly.com/v2/accounts/06a5313b-7972-48a9-a0a9-3d7d741afe44/adjustments"/>
<account_balance href="https://domain.recurly.com/v2/accounts/06a5313b-7972-48a9-a0a9-3d7d741afe44/balance"/>
<billing_info href="https://domain.recurly.com/v2/accounts/06a5313b-7972-48a9-a0a9-3d7d741afe44/billing_info"/>
<!-- ... -->
</account>
01:03 If we scroll down further, we see that a username can be nil
, and
this is represented by an attribute called nil
with nil
as its value:
<username nil="nil"></username>
01:19 By wrapping the specifics of this XML format in a decoder, we can
use a single implementation to decode resources from Recurly — not only on the
server side, but also on the client side — if our app needs to talk with the
same API.
01:44 If we only had to deal with this one response, perhaps it would be
more efficient to parse it manually. But in the long run, writing a decoder is
going to save us a lot of time because it will allow us to decode any struct
from a response from Recurly.
Creating an XML Decoder
02:07 We start out small with a struct based on the sample XML. And to
save some time, we simply use snake-cased property names that match up with the
XML:
struct Account: Codable {
var state: String
var email: String
var company_name: String
}
03:04 We create a decoder by writing a class that conforms to the
Decoder
protocol, and we let the compiler add in the protocol properties and
methods. Just like we did last week, we're going to ignore the codingPath
and
userInfo
properties, and we'll set them to empty values:
final class RecurlyXMLDecoder: Decoder {
var codingPath: [CodingKey] = []
var userInfo: [CodingUserInfoKey:Any] = [:]
// ...
}
03:36 The first thing we have to implement is the method that provides a
keyed container. We do this by returning a KeyedDecodingContainer
— a value
that wraps any type conforming to KeyedDecodingContainerProtocol
. The wrapped
container type has to be generic over a CodingKey
type. We create a struct
called KDC
to serve as the container type:
final class RecurlyXMLDecoder: Decoder {
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
return KeyedDecodingContainer(KDC())
}
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
}
}
04:28 We let the compiler generate the required protocol stubs for the
keyed decoding container, KDC
, and we again ignore the first two properties:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
var codingPath: [CodingKey] = []
var allKeys: [Key] = []
}
04:37 Inside this keyed decoding container, we have to start reading
from the XML. We'll work with one of Foundation's built-in types, XMLElement
,
which is the subclass of XMLNode
that represents a single element. We store an
element as a property of the decoder, and we also pass it into the keyed
decoding container, KDC
:
final class RecurlyXMLDecoder: Decoder {
let element: XMLElement
init(_ element: XMLElement) {
self.element = element
}
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
return KeyedDecodingContainer(KDC(element))
}
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
var codingPath: [CodingKey] = []
var allKeys: [Key] = []
let element: XMLElement
init(_ element: XMLElement) {
self.element = element
}
}
}
05:40 We throw fatal errors in all other methods of KDC
in order to
see which method we have to implement first. In the end, we'll probably need to
implement most of the methods, but this way we don't have to do everything at
once. We also throw fatal errors in the remaining methods of the
RecurlyXMLDecoder
itself.
We have to make sure we don't keep any fatal errors in our production code. This
matters especially for server-side code, because a fatal error stops the
process. So in the end, we have to handle errors in some other way that doesn't
crash the server.
06:58 To start our implementation, we try decoding the sample XML string
and see where we crash first. We create an XMLDocument
from the XML string,
and we pass its root element into our decoder. Then we try to decode an
Account
struct:
struct Account: Codable {
var state: String
var email: String
var company_name: String
}
let document = try XMLDocument(xmlString: xml, options: [])
let root = document.rootElement()!
let decoder = RecurlyXMLDecoder(root)
let account = try Account(from: decoder)
print(account)
08:19 As expected, no account value is printed to the console because we
have run into our first fatal error: the one in the string decoding method of
the keyed decoding container. This is the first piece we need to implement:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func decode(_ type: String.Type, forKey key: Key) throws -> String {
fatalError()
}
}
Decoding Strings
08:37 In order to decode a string, we first try to find the element's
child that has a name that matches with the given key. The element's children
have the common type XMLNode
, so we have to try casting the found child to the
XMLElement
subclass:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func decode(_ type: String.Type, forKey key: Key) throws -> String {
let child = (element.children ?? []).first(where: { $0.name == key.stringValue }).flatMap { $0 as? XMLElement }
}
}
09:50 If we can't find a child with the correct name and type, we have
to throw a DecodingError.keyNotFound
error:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func decode(_ type: String.Type, forKey key: Key) throws -> String {
guard let child = (element.children ?? []).first(where: { $0.name == key.stringValue }).flatMap({ $0 as? XMLElement }) else {
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "TODO"))
}
}
}
10:23 In addition to the key, the error also takes a context parameter
to provide information for debugging, such as the coding key path. But since
we're not populating this path in this demo, we can't really add any more
information to the thrown error.
11:29 If we do find the child element, we return its string value.
XMLNode
's property stringValue
is an optional, but we believe it can never
be nil
for the XMLElement
subclass of XMLNode
. So we force-unwrap it,
which will result in a crash if our assumption is wrong:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func decode(_ type: String.Type, forKey key: Key) throws -> String {
guard let child = return child.stringValue! }
}
12:21 Running the code now, we already get a successfully decoded
Account
, since all its properties are strings:
let account = try Account(from: decoder)
print(account)
Decoding nil
12:30 As a next step, we want to decode nil
in order to have optional
fields on Account
. Currently, we're decoding the company name as an empty
string, but the XML defines this value as nil
:
<company_name nil="nil"></company_name>
13:26 To allow the value to be decoded as nil
, we turn the property
into an optional one:
struct Account: Codable {
var state: String
var email: String
var company_name: String?
}
13:33 Now if we run the app, we crash in another method we haven't
implemented. Before we fix this crash, we can pull out the logic to look up an
XML element's child by a coding key, because our decoder will need it in many
places:
extension XMLElement {
func child(for key: CodingKey) -> XMLElement? {
return (children ?? []).first(where: { $0.name == key.stringValue }).flatMap({ $0 as? XMLElement })
}
}
14:45 This makes the string decoding method much more readable:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func decode(_ type: String.Type, forKey key: Key) throws -> String {
guard let child = element.child(for: key) else {
}
return child.stringValue! }
}
15:01 We use the same helper to implement the next method, contains
,
which should return true
if a child can be found for the given key:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func contains(_ key: Key) -> Bool {
return element.child(for: key) != nil
}
}
15:36 Next, we run into a fatal error in decodeNil
, which should
return true
if the child element for the given key has to be interpreted as
nil
.
If we can't find the child element at all, we should throw an error like before,
and since this logic is the same as in the string decoding method, we can pull
out another helper that does the error throwing for us:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func child(for key: CodingKey) throws -> XMLElement {
guard let child = element.child(for: key) else {
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "TODO"))
}
return child
}
func decode(_ type: String.Type, forKey key: Key) throws -> String {
let child = try self.child(for: key)
return child.stringValue! }
}
17:07 In decodeNil
, we now only have to check whether or not the
found child element has a nil
attribute. If it does, we return true
, which
means we're decoding a nil
value:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func decodeNil(forKey key: Key) throws -> Bool {
let child = try self.child(for: key)
return child.attribute(forName: "nil") != nil
}
}
17:58 It can be very confusing to get this logic right, so we really
like the fact that we're wrapping it in a decoder and we only have to get it
right this one time. We no longer have to worry about the format's specifics
when we actually use the decoder.
18:22 Now when we run the code, instead of an empty string, we
correctly get nil
for the company name property:
let account = try Account(from: decoder)
print(account)
Decoding Nested Values
18:26 For the next step, we can turn the account state into an enum,
which is itself Codable
:
struct Account: Codable {
enum State: String, Codable {
case active, canceled
}
var state: State
var email: String
var company_name: String?
}
19:10 This crashes our code in another method, namely the one that
decodes a nested type:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
fatalError()
}
}
19:18 This method is called because we have to decode a State
value,
but all we know inside the method is that we have to decode a generic, decodable
type T
. This means we can call the initializer T(from: decoder)
, for which
we need a decoder.
Many Decoder
implementations use the root decoder at this point for maximum
efficiency, but we'll create a new one because it's easier and the performance
is good enough for our use case. We search a child whose name matches the given
key and pass it into the new decoder:
struct KDC<Key: CodingKey>: KeyedDecodingContainerProtocol {
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
let el = try child(for: key)
let decoder = RecurlyXMLDecoder(el)
return try T(from: decoder)
}
}
20:43 This time, we run into the fatal error in the decoder's
singleValueContainer
method. This method is called because we're trying to
decode a State
, and the Decodable
implementation that the compiler generated
for this enum asks the decoder for this type of container.
21:22 To implement this method, we need to return a
SingleValueDecodingContainer
, for which we create a struct, SVDC
. And just
like we did with the keyed decoding container, we pass the decoder's XML element
to the struct, and we satisfy the compiler by throwing fatal errors in all
protocol methods:
final class RecurlyXMLDecoder: Decoder {
func singleValueContainer() throws -> SingleValueDecodingContainer {
return SVDC(element)
}
struct SVDC: SingleValueDecodingContainer {
var codingPath: [CodingKey] = []
let element: XMLElement
init(_ element: XMLElement) {
self.element = element
}
}
}
22:43 This builds and we crash in the string decoding method of the
single value container. Unlike in the keyed container, we don't have to look up
a child by a key because we're decoding a single value now. So we simply return
the string value of the root element we're given:
struct SVDC: SingleValueDecodingContainer {
var codingPath: [CodingKey] = []
let element: XMLElement
init(_ element: XMLElement) {
self.element = element
}
func decode(_ type: String.Type) throws -> String {
return element.stringValue! }
}
23:35 When we run the code, we get a successfully decoded State
value:
let account = try Account(from: decoder)
print(account)
Coming Up
23:44 We'll leave it at this for today, but there are still a few
challenges ahead. For one, we have to decode dates that are represented in the
XML as ISO 8601-formatted strings, while the decoder tries to decode a Double
by default.
24:15 Another challenge is the decoding of arrays. XML doesn't really
have an array type — like JSON does — that can be mapped directly to our
decoder. So we have to figure out another way to construct arrays from an XML
document.
24:28 So we can continue working on our decoder until we no longer run
into fatal errors and every method we need is implemented.