Skip to content

CoreOffice/XMLCoder

Repository files navigation

XMLCoder

Encoder & Decoder for XML using Swift'sCodableprotocols.

Version License Platform Coverage

This package is a fork of the original ShawnMoore/XMLParsing with more features and improved test coverage. Automatically generated documentation is available onour GitHub Pages.

Join our Discordfor any questions and friendly banter.

Example

import XMLCoder
import Foundation

letsourceXML="""
<note>
<to>Bob</to>
<from>Jane</from>
<heading>Reminder</heading>
<body>Don't forget to use XMLCoder!</body>
</note>
"""

structNote:Codable{
letto:String
letfrom:String
letheading:String
letbody:String
}

letnote=try!XMLDecoder().decode(Note.self,from:Data(sourceXML.utf8))

letencodedXML=try!XMLEncoder().encode(note,withRootKey:"note")

Advanced features

The following features are available in0.4.0 releaseor later (unless stated otherwise):

Stripping namespace prefix

Sometimes you need to handle an XML namespace prefix, like in the XML below:

<h:tablexmlns: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 shouldProcessNamespacesproperty:

structTable:Codable,Equatable{
structTR:Codable,Equatable{
lettd:[String]
}

lettr:[TR]
}


letdecoder=XMLDecoder()

// Setting this property to `true` for the namespace prefix to be stripped
// during decoding so that key names could match.
decoder.shouldProcessNamespaces=true

letdecoded=trydecoder.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:DynamicNodeEncodingand DynamicNodeDecoding.

The declarations of the protocols are very simple:

protocolDynamicNodeEncoding:Encodable{
staticfuncnodeEncoding(for key:CodingKey)->XMLEncoder.NodeEncoding
}

protocolDynamicNodeDecoding:Decodable{
staticfuncnodeDecoding(for key:CodingKey)->XMLDecoder.NodeDecoding
}

The values returned by correspondingstaticfunctions look like this:

enumNodeDecoding{
// decodes a value from an attribute
caseattribute

// decodes a value from an element
caseelement

// the default, attempts to decode as an element first,
// otherwise reads from an attribute
caseelementOrAttribute
}

enumNodeEncoding{
// encodes a value in an attribute
caseattribute

// the default, encodes a value in an element
caseelement

// encodes a value in both attribute and element
caseboth
}

Add conformance to an appropriate protocol for types you'd like to customize. Accordingly, this example code:

structBook:Codable,Equatable,DynamicNodeEncoding{
letid:UInt
lettitle:String
letcategories:[Category]

enumCodingKeys:String,CodingKey{
caseid
casetitle
casecategories="category"
}

staticfuncnodeEncoding(for key:CodingKey)->XMLEncoder.NodeEncoding{
switch key{
caseBook.CodingKeys.id:return.both
default:return.element
}
}
}

works for this XML:

<bookid="123">
<id>123</id>
<title>Cat in the Hat</title>
<category>Kids</category>
<category>Wildlife</category>
</book>

Please refer to PR#70by @JoeMattfor more details.

Coding key value intrinsic

Suppose that you need to decode an XML that looks similar to this:

<?xmlversion="1.0"encoding="UTF-8"?>
<fooid="123">456</foo>

By default you'd be able to decodefooas an element, but then it's not possible to decode theidattribute.XMLCoderhandles certainCodingKey values in a special way to allow proper coding for this XML. Just add a coding key withstringValuethat 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.

structFoo:Codable,DynamicNodeEncoding{
letid:String
letvalue:String

enumCodingKeys:String,CodingKey{
caseid
casevalue=""
}

staticfuncnodeEncoding(forKey key:CodingKey)
->XMLEncoder.NodeEncoding{
switch key{
caseCodingKeys.id:
return.attribute
default:
return.element
}
}
}

Thanks to@JoeMattfor 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 withvalue intrinsic keys. Starting withversion 0.5 you can now set a propertytrimValueWhitespacestofalse(the default value istrue) on XMLDecoderinstance to preserve all whitespaces in decoded strings.

Remove whitespace elements

When decoding pretty-printed XML whiletrimValueWhitespacesis set tofalse,it's possible for whitespace elements to be added as child elements on an instance ofXMLCoderElement.These whitespace elements make it impossible to decode data structures that require customDecodablelogic. Starting withversion 0.13.0you can set a propertyremoveWhitespaceElementstotrue(the default value isfalse) on XMLDecoderto remove these whitespace elements.

Choice element coding

Starting withversion 0.8, you can encode and decodeenums with associated values by conforming your CodingKeytype additionally toXMLChoiceCodingKey.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:

enumIntOrString:Codable{
caseint(Int)
casestring(String)

enumCodingKeys:String,XMLChoiceCodingKey{
caseint
casestring
}

enumIntCodingKeys:String,CodingKey{case_0=""}
enumStringCodingKeys:String,CodingKey{case_0=""}
}

This is described in more details in PR#119 by@jsbeanand@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>
<nestedattr="n1_a1">
<val>n1_v1</val>
<labeled>
<val>n2_val</val>
</labeled>
</nested>
<simpleattr="n1_a1">
<val>n1_v1</val>
</simple>
</container>
enumInlineChoice:Equatable,Codable{
casesimple(Nested1)
casenested(Nested1,labeled:Nested2)

enumCodingKeys:String,CodingKey,XMLChoiceCodingKey{
casesimple,nested
}

enumSimpleCodingKeys:String,CodingKey{case_0=""}

enumNestedCodingKeys:String,CodingKey{
case_0=""
caselabeled
}

structNested1:Equatable,Codable,DynamicNodeEncoding{
varattr="n1_a1"
varval="n1_v1"

publicstaticfuncnodeEncoding(for key:CodingKey)->XMLEncoder.NodeEncoding{
switch key{
caseCodingKeys.attr:return.attribute
default:return.element
}
}
}

structNested2:Equatable,Codable{
varval="n2_val"
}
}

Integrating withCombine

Starting with XMLCoderversion 0.9, when Apple's Combine framework is available,XMLDecoderconforms to the TopLevelDecoderprotocol, which allows it to be used with the decode(type:decoder:)operator:

import Combine
import Foundation
import XMLCoder

funcfetchBook(from url:URL)->AnyPublisher<Book,Error>{
returnURLSession.shared.dataTaskPublisher(for:url)
.map(\.data)
.decode(type:Book.self,decoder:XMLDecoder())
.eraseToAnyPublisher()
}

This was implemented in PR#132 by@sharplet.

Additionally, starting withXMLCoder 0.11XMLEncoder conforms to theTopLevelEncoderprotocol:

import Combine
import XMLCoder

funcencode(book:Book)->AnyPublisher<Data,Error>{
returnJust(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 appropriatekeyEncodingstrategy 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 theTopLevelEncoderAPI.

Root element attributes

Sometimes you need to set attributes on the root element, which aren't directly related to your model type. Starting withXMLCoder 0.11theencode function onXMLEncoderaccepts a newrootAttributesargument to help with this:

structPolicy:Encodable{
varname:String
}

letencoder=XMLEncoder()
letdata=tryencoder.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:

<policyxmlns="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 by@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.

For example, this type

structBook:Codable{
@Elementvarid:Int
}

will encode valueBook(id: 42)as<Book><id>42</id></Book>.And vice versa, it will decode the latter into the former.

Similarly,

structBook:Codable{
@Attributevarid:Int
}

will encode valueBook(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:

structBook:Codable{
@ElementAndAttributevarid:Int
}

This will encode valueBook(id: 42)as<Book id= "42" ><id>42</id></Book>.It will decode both <Book><id>42</id></Book>and<Book id= "42" ></Book>asBook(id: 42).

This feature is available starting with XMLCoder 0.13.0 and was implemented by@bwetherfield.

XML Headers

You can add an XML header and/or doctype when encoding an object by supplying it to theencodefunction. These arguments are both optional, and will only render when explicitly provided.

structUser:Codable{
@Elementvarusername:String
}

letdata=tryencoder.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 withEXC_BAD_ACCESScaused bya compiler bug,please use Xcode 11.3 or later instead. Please refer to#150for 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 Manageris 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, addingXMLCoderas a dependency is as easy as adding it to thedependenciesvalue of yourPackage.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 dependencyusing Xcode's GUI.

CocoaPods

CocoaPodsis a dependency manager for Swift and Objective-C Cocoa projects for Apple's platfoms. You can install it with the following command:

$ gem install cocoapods

Navigate to the project directory and createPodfilewith the following command:

$ pod install

Inside of yourPodfile,specify theXMLCoderpod:

# 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'
end

Then, run the following command:

$ pod install

Open the theYourApp.xcworkspacefile that was created. This should be the file you use everyday to create your app, instead of theYourApp.xcodeproj file.

Carthage

Carthageis a dependency manager for Apple's platfoms that builds your dependencies and provides you with binary frameworks.

Carthage can be installed withHomebrewusing the following command:

$ brew update
$ brew install carthage

Inside of yourCartfile,add GitHub path toXMLCoder:

github "CoreOffice/XMLCoder" ~> 0.15.0

Then, run the following command to build the framework:

$ carthage update

Drag the built framework into your Xcode project.

Usage with Vapor

extensionXMLEncoder:ContentEncoder{
publicfuncencode<E:Encodable>(
_ encodable:E,
to body:inoutByteBuffer,
headers:inoutHTTPHeaders
)throws{
headers.contentType=.xml

// Note: You can provide an XMLHeader or DocType if necessary
letdata=tryself.encode(encodable)
body.writeData(data)
}
}

extensionXMLDecoder:ContentDecoder{
publicfuncdecode<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`
letbody=body.readData(length:body.readableBytes)!
returntryself.decode(D.self,from:body)
}
}

Contributing

This project adheres to theContributor Covenant Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to[email protected].

Coding Style

This project usesSwiftFormat andSwiftLintto 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 anXcode extension, build phaseor git pre-commit hooketc.

To guarantee that these tools run before you commit your changes on macOS, you're encouraged to run this once to set up thepre-commithook:

brew bundle # installs SwiftLint, SwiftFormat and pre-commit
pre-commit install # installs pre-commit hook to run checks before you commit

Refer tothe pre-commit documentation pagefor 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 toXML 1.0 standard.All of this can be easily tested automatically and we're slowly improvingtest coverage of XMLCoderand 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.