SkillAgentSearch skills...

XMLCoder

Easy XML parsing using Codable protocols in Swift

Install / Use

/learn @CoreOffice/XMLCoder
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

XMLCoder

Encoder & Decoder for XML using Swift's Codable protocols.

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 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

View on GitHub
GitHub Stars866
CategoryDevelopment
Updated14d ago
Forks121

Languages

Swift

Security Score

100/100

Audited on Mar 17, 2026

No findings