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.1, 5.2.
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
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.
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.
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
}
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
}
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.
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.
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>
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).
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>
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.
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.
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.
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.
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.
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.
The current version encodes and decodes data as Base64 strings. No customization option is provided.
This is subject to change in a future implementation.
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.
URLs are converted to/from strings. No customization option is provided.