This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We implement a custom XML decoder that allows us to decode responses from an XML API using Decodable.

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! // todo verify that it's never nil
    }

    // ...
}

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)

// Account(state: "active", email: "mail@floriankugler.com", company_name: "")

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! // todo verify that it's never nil
    }

    // ...
}

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! // todo verify that it's never nil
    }

    // ...
}

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)

// Account(state: "active", email: "mail@floriankugler.com", company_name: nil)

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! // todo check "never nil" assumption
    }

    // ...
}

23:35 When we run the code, we get a successfully decoded State value:

let account = try Account(from: decoder)
print(account)

// Account(state: XMLDecoder.Account.State.active, email: "mail@floriankugler.com", company_name: nil)

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.

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes