/XMLCoder

Swift Encoder and Decoder for XML documents

Primary LanguageSwiftBSD 4-Clause "Original" or "Old" LicenseBSD-4-Clause

XMLCoder

This package allows to encode/decode arbitrary XML documents through the Codable protocols introduced with Swift 4.

Disclaimer: I developed this package in order to use it in my own projects. I'm releasing it as open source with the hope it can be useful to other developers. While I designed this implementation to be reasonably generic, I don't claim it will fit all needs. However, if you find a bug or miss a feature, you are welcome to submit an issue or a pull request in the form of a failing test (or even better, a bug fix or working feature). I am open to all suggestions.

Supported Platforms: macOS 10.12+, Ubuntu 18.04, Ubuntu 20.04.

Supported Swift versions: 5.1 ... 5.6.

Usage

Integration (Package.swift)

CodableXML relies on the Swift Package Manager for integration in your projects.

  • add this line to the dependencies list in your Package.swift file:
.package(url: "https://github.com/franklefebvre/XMLCoder.git", equal: "0.3.3"),
  • add XMLCoder dependency to your target:
.target(
    name: <your_target_here>,
    dependencies: ["XMLCoder", ...]),
  • in the source files where the module is used: import XMLCoder

Integration (Xcode 11+)

You can add XMLCoder to an existing project by selecting File > Swift Packages > Add Package Dependency... and by providing the URL of the repository: https://github.com/franklefebvre/XMLCoder. The current version is still a work in progress and some parts of the API may change, therefore I recommend to select "Up to Next Minor" or "Exact" in the Version menu.

Input/Output

The library encodes to / decodes from XMLDocument objects provided by Foundation.

There are many reasons why I chose not to deal with text (or data) for XML:

  • parsing is hard (and it may be a security liability). There are good well-tested libraries that can do the job, such as libxml2, which is used by Foundation (FoundationXML on Linux) behind the scenes.
  • it can be useful to postprocess the generated XML, for example to put it in some canonical form before signing it (yes, SAML, I'm looking at you). In that case it's a lot easier to deal with a structured object tree than with raw XML in text form.

The downside is that the open source implementation of Foundation used on Linux is not identical to the macOS version. The XML structures generated by the encoder may be slightly different.

Encoding

Assuming Document conforms to Encodable, encoding can be as simple as this:

func encode(doc: Document) throws -> String {
    let encoder = XMLEncoder(documentRootTag: "root")
    let xmlDocument = try encoder.encode(doc)
    // here xmlDocument is of type XMLDocument
    guard let result = String(data: xmlDocument.xmlData, encoding: .utf8) else {
        throw SomeError()
    }
    return result
}

Decoding

Assuming Document conforms to Decodable, an XML string can be decoded this way:

func decode(xmlString: String) throws -> Document {
    let xmlDocument = XMLDocument(...)
    let decoder = XMLDecoder()
    let document = try decoder.decode(Document.self, from: xmlDocument)
    return document
}

XML Features

XMLCoder supports most XML-specific features such as namespaces, attributes, etc. These features are implemented through extensions to the CodingKeys protocol.

It is possible to encode/decode a simple XML document by taking advantage of code synthesized by the compiler for your Codable types. However, when the XML representation is more complex, it is necessary to provide explicit CodingKeys declarations alongside the codable types.

Namespaces

In order to support namespaces, the CodingKeys type must comply to the XMLQualifiedKey protocol.

This protocol is declared as follows:

protocol XMLQualifiedKey {
    var namespace: String? { get }
}

Thus, for each key defined in the CodingKeys type, namespace can return either a namespace URI, or nil if the key belongs to the default namespace.

Namespace Example

struct NamespaceStruct: Codable {
    var key1: String
    var key2: String
    
    private enum CodingKeys: String, CodingKey, XMLQualifiedKey {
        case key1
        case key2
        
        var namespace: String? {
            switch self {
            case .key1:
                return "http://namespace.example.com"
            default:
                return nil
            }
        }
    }
}

let value = NamespaceStruct(key1: "element1", key2: "element2")
let encoder = XMLEncoder(documentRootTag: "root")
let xmlDocument = try encoder.encode(value)
let result = String(data: xmlDocument.xmlData, encoding: .utf8)

The result string will contain something like:

<root xmlns:ns1="http://namespace.example.com">
    <ns1:key1>element1</ns1:key1>
    <key2>element2</key2>
</root>

Node Types

Attributes and arrays are implemented by adding the XMLTypedKey protocol to CodingKeys.

protocol XMLTypedKey {
    var nodeType: XMLNodeType { get }
}

enum XMLNodeType {
    case element
    case attribute
    case inline
    case array(String?)
}
  • When a key is defined as element, its value is represented as an XML element. This is the default behavior.
  • When a key is defined as attribute, it becomes an attribute of the parent node (see "Attribute Example" below).
  • When a key is defined as inline, its value is represented as a string without an enclosing element (ie, it belongs to the parent element).
  • When a key is defined as array, its value is represented as a sequence of XML elements whose keys are the associated value of the enum case, defaulting to element (see "Array Example below).

Attribute Example

With this definition:

struct AttributeStruct: Codable {
    var key1: String
    var key2: String
    
    private enum CodingKeys: String, CodingKey, XMLTypedKey {
        case key1
        case key2
        
        var nodeType: XMLNodeType {
            switch self {
            case .key1:
                return .attribute
            case .key2:
                return .element
            }
        }
    }
}

let value = AttributeStruct(key1: "value1", key2: "value2")
let encoder = XMLEncoder(documentRootTag: "root")
let xmlDocument = try encoder.encode(value)
let result = String(data: xmlDocument.xmlData, encoding: .utf8)

The result string will contain something like:

<root key1="value1">
    <key2>value2</key2>
</root>

Array Example

With this definition:

struct ArrayStruct: Codable {
    var key3: String
    var key4: [String]
    
    private enum CodingKeys: String, CodingKey, XMLNodeType {
        case key3
        case key4
        
        var nodeType: XMLNodeType {
            switch(self) {
            case .key3:
                return .element
            case .key4:
                return .array("child")
            }
        }
    }
}

let value = ArrayStruct(key3: "value3", key4: ["one", "two", "three"])
let encoder = XMLEncoder(documentRootTag: "root")
let xmlDocument = try encoder.encode(value)
let result = String(data: xmlDocument.xmlData, encoding: .utf8)

The result string will contain something like:

<root>
    <key3>value3</key3>
    <key4>
        <child>one</child>
        <child>two</child>
        <child>three</child>
    </key4>
</root>

This is subject to change in a future implementation.

Root Element

XMLEncoder must be initialized with the name of tag representing the document root.

By default XMLDecoder ignores the root tag in the XML document. However it is possible to specify the expected name of the root tag with the documentRootTag property.

Coding Strategies

Key transformations

By default, keys in Swift types are encoded and decoded as XML tags with the same names, without transformation. However it is possible to change this behavior globally by using the keyCodingStrategy, elementNameCodingStrategy and attributeNameCodingStrategy attributes on XMLEncoder and XMLDecoder.

keyCodingStrategy defines the behavior for elements and attributes, unless overridden by elementNameCodingStrategy or attributeNameCodingStrategy.

Possible settings are:

  • useDefaultKeys: this is the default behavior, as described above.
  • custom: allows to provide a closure to implement the transformation. The closure always defines the transform from the coding keys to the XML element and/or attribute names. This allows to use the same function both for encoding and for decoding.

Besides this global mechanism, it is still possible to convert between keys and tags on a case by base basis, e.g. by providing explicitly strings for the enum cases in the CodingKeys enum.

Value conversions

Some types don't have a native representation in XML, and may require additional transformations in order to be represented as strings in XML elements or attributes.

Nil

XMLEncoder's nilEncodingStrategy and XMLDecoder's nilDecodingStrategy determine how optional values are represented in the XML document.

Possible settings are:

  • missing: the XML element or attribute corresponding to a nil value is not present in the XML document. This is the default behavior.
  • empty: a nil value is represented in the XML document by either an empty element or an attribute whose value is the empty string.

Bool

XMLEncoder exposes a boolEncodingStrategy property, and XMLDecoder exposes a symmetrical boolDecodingStrategy property. These properties are defined as two-element structs containing the strings used to represent false and true values.

By default false is represented as "0", and true is represented as "1".

Data

XMLEncoder exposes a dataEncodingStrategy property, and XMLDecoder exposes a symmetrical dataDecodingStrategy property. These properties allow to represent Data properties as Base64 strings (the default), hexadecimal strings, or to provide a custom implementation.

Date

XMLEncoder exposes a dateEncodingStrategy property, and XMLDecoder exposes a symmetrical dateDecodingStrategy property. The default behavior is to encode and decode dates as ISO 8601 strings; other strategies allow to provide a DateFormatter or a custom implementation.

URL

URLs are converted to/from strings. No customization option is provided.