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 yourPackage.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 toelement
(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.