CoreOffice/XMLCoder
Easy XML parsing using Codable protocols in Swift
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](https://github.com/CoreOffice/XMLCoder/releases/tag/0.4.0) or later
(unless stated otherwise):
### Stripping namespace prefix
Sometimes you need to handle an XML namespace prefix, like in the XML below:
```xml
<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:
```swift
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:
```swift
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:
```swift
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:
```swift
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:
```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](https://github.com/CoreOffice/XMLCoder/pull/70) by
[@JoeMatt](https://github.com/JoeMatt) for more details.
### Coding key value intrinsic
Suppose that you need to decode an XML that looks similar to this:
```xml
<?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.
```swift
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](https://github.com/JoeMatt) for implementing this in
in PR [\#73](https://github.com/CoreOffice/XMLCoder/pull/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](#coding-key-value-intrinsic).
Starting with [version 0.5](https://github.com/CoreOffice/XMLCoder/releases/tag/0.5.0)
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](https://github.com/CoreOffice/XMLCoder/releases/tag/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](https://github.com/CoreOffice/XMLCoder/releases/tag/0.8.0),
you can encode and decode `enum`s with associated values by conforming your
`CodingKey` type additionally to `XMLChoiceCodingKey`. This allows encoding
and decoding XML elements similar in structure to this example:
```xml
<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:
```swift
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](https://github.com/CoreOffice/XMLCoder/pull/119)
by [@jsbean](https://github.com/jsbean) and [@bwetherfield](https://github.com/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:
```xml
<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>
```
```swift
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](https://developer.apple.com/documentation/combine)
Starting with XMLCoder [version 0.9](https://github.com/CoreOffice/XMLCoder/releases/tag/0.9.0),
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:
```swift
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](https://github.com/CoreOffice/XMLCoder/pull/132)
by [@sharplet](https://github.com/sharplet).
Additionally, starting with [XMLCoder
0.11](https://github.com/CoreOffice/XMLCoder/releases/tag/0.11.0) `XMLEncoder`
conforms to the `TopLevelEncoder` protocol:
```swift
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`, to customize
capitalization of the root element (e.g. `<Book`) you'll need to set an
appropriate `keyEncoding` strategy on the encoder. To change the element name
altogether you'll have to change the name of the type, which is an unfortunate
limitation of the `TopLevelEncoder` API.
### Root element attributes
Sometimes you need to set attributes on the root element, which aren't
directly related to your model type. Starting with [XMLCoder
0.11](https://github.com/CoreOffice/XMLCoder/releases/tag/0.11.0) the `encode`
function on `XMLEncoder` accepts a new `rootAttributes` argument to help with
this:
```swift
struct Policy: Encodable {
var name: String
}
let encoder = XMLEncoder()
let data = try encoder.encode(Policy(name: "test"), rootAttributes: [
"xmlns": "http://www.nrf-arts.org/IXRetail/namespace",
"xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
])
```
The resulting XML will look like this:
```xml
<policy xmlns="http://www.nrf-arts.org/IXRetail/namespace"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<name>test</name>
</policy>
```
This was implemented in PR [\#160](https://github.com/CoreOffice/XMLCoder/pull/160)
by [@portellaa](https://github.com/portellaa).
### Property wrappers
If your version of Swift allows property wrappers to be used, you may prefer this API to the more verbose
[dynamic node coding](#dynamic-node-coding).
For example, this type
```swift
struct Book: Codable {
@Element var id: Int
}
```
will encode value `Book(id: 42)` as `<Book><id>42</id></Book>`. And vice versa,
it will decode the latter into the former.
Similarly,
```swift
struct Book: Codable {
@Attribute var id: Int
}
```
will encode value `Book(id: 42)` as `<Book id="42"></Book>` and vice versa for decoding.
If you don't know upfront if a property will be present as an element or an attribute during decoding,
use `@ElementAndAttribute`:
```swift
struct Book: Codable {
@ElementAndAttribute var id: Int
}
```
This will encode value `Book(id: 42)` as `<Book id="42"><id>42</id></Book>`. It will decode both
`<Book><id>42</id></Book>` and `<Book id="42"></Book>` as `Book(id: 42)`.
This feature is available starting with XMLCoder 0.13.0 and was implemented
by [@bwetherfield](https://github.com/bwetherfield).
### XML Headers
You can add an XML header and/or doctype when encoding an object by supplying it to the `encode` function.
These arguments are both optional, and will only render when explicitly provided.
```swift
struct User: Codable {
@Element var username: String
}
let data = try encoder.encode(
User(username: "Joanis"),
withRootKey: "user",
header: XMLHeader(version: 1.0, encoding: "UTF-8"),
doctype: .system(
rootElement: "user",
dtdLocation: "http://example.com/myUser_v1.dtd"
)
)
```Installation
Requirements
Apple Platforms
- Xcode 11.0 or later
- IMPORTANT: compiling XMLCoder with Xcode 11.2.0 (11B52) and 11.2.1 (11B500) is not recommended due to crashes with EXC_BAD_ACCESS caused by a compiler bug, please use Xcode 11.3 or later instead. Please refer to \#150 for more details.
- Swift 5.1 or later
- iOS 9.0 / watchOS 2.0 / tvOS 9.0 / macOS 10.10 or later deployment targets
Linux
- Ubuntu 18.04 or later
- Swift 5.1 or later
Windows
- Swift 5.5 or later.
Swift Package Manager
Swift Package Manager is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies on all platforms.
Once you have your Swift package set up, adding XMLCoder as a dependency is as easy as adding it to the dependencies value of your Package.swift.
dependencies: [
.package(url: "https://github.com/CoreOffice/XMLCoder.git", from: "0.15.0")
]If you're using XMLCoder in an app built with Xcode, you can also add it as a direct dependency using Xcode's GUI.
CocoaPods
CocoaPods is a dependency manager for Swift and Objective-C Cocoa projects for Apple's platfoms. You can install it with the following command:
$ gem install cocoapodsNavigate to the project directory and create Podfile with the following command:
$ pod installInside of your Podfile, specify the XMLCoder pod:
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'YourApp' do
# Comment the next line if you're not using Swift or don't want
# to use dynamic frameworks
use_frameworks!
# Pods for YourApp
pod 'XMLCoder', '~> 0.14.0'
endThen, run the following command:
$ pod installOpen the the YourApp.xcworkspace file that was created. This should be the file you use everyday to create your app, instead of the YourApp.xcodeproj file.
Carthage
Carthage is a dependency manager for Apple's platfoms that builds your dependencies and provides you with binary frameworks.
Carthage can be installed with Homebrew using the following command:
$ brew update
$ brew install carthageInside of your Cartfile, add GitHub path to XMLCoder:
github "CoreOffice/XMLCoder" ~> 0.15.0Then, run the following command to build the framework:
$ carthage updateDrag the built framework into your Xcode project.
Usage with Vapor
extension XMLEncoder: ContentEncoder {
public func encode<E: Encodable>(
_ encodable: E,
to body: inout ByteBuffer,
headers: inout HTTPHeaders
) throws {
headers.contentType = .xml
// Note: You can provide an XMLHeader or DocType if necessary
let data = try self.encode(encodable)
body.writeData(data)
}
}
extension XMLDecoder: ContentDecoder {
public func decode<D: Decodable>(
_ decodable: D.Type,
from body: ByteBuffer,
headers: HTTPHeaders
) throws -> D {
// Force wrap is acceptable, as we're guaranteed these bytes exist through `readableBytes`
let body = body.readData(length: body.readableBytes)!
return try self.decode(D.self, from: body)
}
}Contributing
This project adheres to the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to coreoffice@desiatov.com.
Coding Style
This project uses SwiftFormat and SwiftLint to enforce formatting and coding style. We encourage you to run SwiftFormat within a local clone of the repository in whatever way works best for you either manually or automatically via an Xcode extension, build phase or git pre-commit hook etc.
To guarantee that these tools run before you commit your changes on macOS, you're encouraged to run this once to set up the pre-commit hook:
brew bundle # installs SwiftLint, SwiftFormat and pre-commit
pre-commit install # installs pre-commit hook to run checks before you commitRefer to the pre-commit documentation page for more details and installation instructions for other platforms.
SwiftFormat and SwiftLint also run on CI for every PR and thus a CI build can fail with incosistent formatting or style. We require CI builds to pass for all PRs before merging.
Test Coverage
Our goal is to keep XMLCoder stable and to serialize any XML correctly according to XML 1.0 standard. All of this can be easily tested automatically and we're slowly improving test coverage of XMLCoder and don't expect it to decrease. PRs that decrease the test coverage have a much lower chance of being merged. If you add any new features, please make sure to add tests, likewise for changes and any refactoring in existing code.
Package Metadata
Repository: CoreOffice/XMLCoder
Homepage: https://swiftpackageindex.com/CoreOffice/XMLCoder
Stars: 873
Forks: 123
Open issues: 61
Default branch: main
Primary language: swift
License: MIT
Topics: carthage, cocoapods, codable, decoder, encoder, swift, swift4, xml, xml-parser, xml-serializer
README: README.md