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+, iOS 10+, Ubuntu 18.04.
Supported Swift versions: 5.0, 5.1.
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.0"),
- 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 URLo f 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, XMLNodeType {
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 keyEncodingStrategy
attribute on XMLEncoder
, and the keyDecodingStrategy
attribute on XMLDecoder
.
Possible settings are:
useDefaultKeys
: this is the default behavior, as described above.convertToSnakeCase
,convertFromSnakeCase
: allow to represent the keys in camelCase in the Swift types, and to have snake_case tags in the XML document.custom
: allows to provide a closure to implement the transformations.
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"
.
This is subject to change in a future implementation.
Data
The current version encodes and decodes data as Base64 strings. No customization option is provided.
This is subject to change in a future implementation.
Date
The current version encodes and decodes dates as ISO 8601 strings, using the UTC time zone. No customization option is provided.
This is subject to change in a future implementation.
URL
URLs are converted to/from strings. No customization option is provided.