XMLCoder
Easy XML parsing using Codable protocols in Swift
Install / Use
/learn @CoreOffice/XMLCoderREADME
XMLCoder
Encoder & Decoder for XML using Swift's Codable protocols.
This package is a fork of the original ShawnMoore/XMLParsing with more features and improved test coverage. Automatically generated documentation is available on our GitHub Pages.
Join our Discord for any questions and friendly banter.
Example
import XMLCoder
import Foundation
let sourceXML = """
<note>
<to>Bob</to>
<from>Jane</from>
<heading>Reminder</heading>
<body>Don't forget to use XMLCoder!</body>
</note>
"""
struct Note: Codable {
let to: String
let from: String
let heading: String
let body: String
}
let note = try! XMLDecoder().decode(Note.self, from: Data(sourceXML.utf8))
let encodedXML = try! XMLEncoder().encode(note, withRootKey: "note")
Advanced features
The following features are available in 0.4.0 release or later (unless stated otherwise):
Stripping namespace prefix
Sometimes you need to handle an XML namespace prefix, like in the XML below:
<h:table xmlns:h="http://www.w3.org/TR/html4/">
<h:tr>
<h:td>Apples</h:td>
<h:td>Bananas</h:td>
</h:tr>
</h:table>
Stripping the prefix from element names is enabled with
shouldProcessNamespaces property:
struct Table: Codable, Equatable {
struct TR: Codable, Equatable {
let td: [String]
}
let tr: [TR]
}
let decoder = XMLDecoder()
// Setting this property to `true` for the namespace prefix to be stripped
// during decoding so that key names could match.
decoder.shouldProcessNamespaces = true
let decoded = try decoder.decode(Table.self, from: xmlData)
Dynamic node coding
XMLCoder provides two helper protocols that allow you to customize whether nodes
are encoded and decoded as attributes or elements: DynamicNodeEncoding and
DynamicNodeDecoding.
The declarations of the protocols are very simple:
protocol DynamicNodeEncoding: Encodable {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}
protocol DynamicNodeDecoding: Decodable {
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
}
The values returned by corresponding static functions look like this:
enum NodeDecoding {
// decodes a value from an attribute
case attribute
// decodes a value from an element
case element
// the default, attempts to decode as an element first,
// otherwise reads from an attribute
case elementOrAttribute
}
enum NodeEncoding {
// encodes a value in an attribute
case attribute
// the default, encodes a value in an element
case element
// encodes a value in both attribute and element
case both
}
Add conformance to an appropriate protocol for types you'd like to customize. Accordingly, this example code:
struct Book: Codable, Equatable, DynamicNodeEncoding {
let id: UInt
let title: String
let categories: [Category]
enum CodingKeys: String, CodingKey {
case id
case title
case categories = "category"
}
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case Book.CodingKeys.id: return .both
default: return .element
}
}
}
works for this XML:
<book id="123">
<id>123</id>
<title>Cat in the Hat</title>
<category>Kids</category>
<category>Wildlife</category>
</book>
Please refer to PR #70 by @JoeMatt for more details.
Coding key value intrinsic
Suppose that you need to decode an XML that looks similar to this:
<?xml version="1.0" encoding="UTF-8"?>
<foo id="123">456</foo>
By default you'd be able to decode foo as an element, but then it's not
possible to decode the id attribute. XMLCoder handles certain CodingKey
values in a special way to allow proper coding for this XML. Just add a coding
key with stringValue that equals "" (empty string). What
follows is an example type declaration that encodes the XML above, but special
handling of coding keys with those values works for both encoding and decoding.
struct Foo: Codable, DynamicNodeEncoding {
let id: String
let value: String
enum CodingKeys: String, CodingKey {
case id
case value = ""
}
static func nodeEncoding(forKey key: CodingKey)
-> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.id:
return .attribute
default:
return .element
}
}
}
Thanks to @JoeMatt for implementing this in in PR #73.
Preserving whitespaces in element content
By default whitespaces are trimmed in element content during decoding. This
includes string values decoded with value intrinsic keys.
Starting with version 0.5
you can now set a property trimValueWhitespaces to false (the default value is true) on
XMLDecoder instance to preserve all whitespaces in decoded strings.
Remove whitespace elements
When decoding pretty-printed XML while trimValueWhitespaces is set to false, it's possible
for whitespace elements to be added as child elements on an instance of XMLCoderElement. These
whitespace elements make it impossible to decode data structures that require custom Decodable logic.
Starting with version 0.13.0 you can
set a property removeWhitespaceElements to true (the default value is false) on
XMLDecoder to remove these whitespace elements.
Choice element coding
Starting with version 0.8,
you can encode and decode enums with associated values by conforming your
CodingKey type additionally to XMLChoiceCodingKey. This allows encoding
and decoding XML elements similar in structure to this example:
<container>
<int>1</int>
<string>two</string>
<string>three</string>
<int>4</int>
<int>5</int>
</container>
To decode these elements you can use this type:
enum IntOrString: Codable {
case int(Int)
case string(String)
enum CodingKeys: String, XMLChoiceCodingKey {
case int
case string
}
enum IntCodingKeys: String, CodingKey { case _0 = "" }
enum StringCodingKeys: String, CodingKey { case _0 = "" }
}
This is described in more details in PR #119 by @jsbean and @bwetherfield.
Choice elements with (inlined) complex associated values
Lets extend previous example replacing simple types with complex in assosiated values. This example would cover XML like:
<container>
<nested attr="n1_a1">
<val>n1_v1</val>
<labeled>
<val>n2_val</val>
</labeled>
</nested>
<simple attr="n1_a1">
<val>n1_v1</val>
</simple>
</container>
enum InlineChoice: Equatable, Codable {
case simple(Nested1)
case nested(Nested1, labeled: Nested2)
enum CodingKeys: String, CodingKey, XMLChoiceCodingKey {
case simple, nested
}
enum SimpleCodingKeys: String, CodingKey { case _0 = "" }
enum NestedCodingKeys: String, CodingKey {
case _0 = ""
case labeled
}
struct Nested1: Equatable, Codable, DynamicNodeEncoding {
var attr = "n1_a1"
var val = "n1_v1"
public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.attr: return .attribute
default: return .element
}
}
}
struct Nested2: Equatable, Codable {
var val = "n2_val"
}
}
Integrating with Combine
Starting with XMLCoder version 0.9,
when Apple's Combine framework is available, XMLDecoder conforms to the
TopLevelDecoder protocol, which allows it to be used with the
decode(type:decoder:) operator:
import Combine
import Foundation
import XMLCoder
func fetchBook(from url: URL) -> AnyPublisher<Book, Error> {
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Book.self, decoder: XMLDecoder())
.eraseToAnyPublisher()
}
This was implemented in PR #132 by @sharplet.
Additionally, starting with XMLCoder
0.11 XMLEncoder
conforms to the TopLevelEncoder protocol:
import Combine
import XMLCoder
func encode(book: Book) -> AnyPublisher<Data, Error> {
return Just(book)
.encode(encoder: XMLEncoder())
.eraseToAnyPublisher()
}
The resulting XML in the example above will start with <book
