mfranceschi6/swift-xml-coder
A high-performance, Codable-compatible XML encoder and decoder for Swift, backed by libxml2.
Features
XMLEncoder/XMLDecoder— Codable-compatible, zero-reflection encoding and decoding- Three-tier field mapping —
@XMLAttribute/@XMLChildproperty wrappers,@XMLCodablemacros (Swift 5.9+), or runtimeXMLFieldCodingOverrides - Streaming —
XMLStreamParser(push/SAX),XMLStreamWriter,XMLItemDecoder(item-by-item Codable decode) - XPath 1.0 — query parsed documents with namespace-aware expressions
- Namespace support — declare, resolve, and validate XML namespace prefixes; per-field namespace override via
XMLFieldNamespaceProvider/@XMLFieldNamespace - Canonicalization — deterministic XML output via
XMLCanonicalizer(XML-DSig ready) - Parser security — configurable depth, node-count, and text-size limits; network and DTD access disabled by default
- Structured diagnostics —
XMLParsingError.decodeFailedwith coding path andXMLSourceLocationfor precise error reporting - Immutable tree model — value-semantic
XMLTreeDocumentfor transform pipelines; full fidelity for processing instructions, doctypes, and comments - Swift 5.6 – 6.1 — multi-manifest compatibility; macros on 5.9+;
~Copyableownership on 6.0+ - macOS, iOS, tvOS, watchOS, Linux — SPM-only, no Objective-C, no Foundation XML APIs
Installation
// Package.swift
dependencies: [
.package(url: "https://github.com/MFranceschi6/swift-xml-coder.git", from: "2.0.0")
],
targets: [
.target(
name: "MyTarget",
dependencies: [
.product(name: "SwiftXMLCoder", package: "swift-xml-coder"),
// Optional: macro support (Swift 5.9+)
.product(name: "SwiftXMLCoderMacros", package: "swift-xml-coder"),
]
)
]Quick Start
Encode
import SwiftXMLCoder
struct Book: Codable {
var title: String
var author: String
var year: Int
}
let book = Book(title: "Swift in Practice", author: "Apple", year: 2024)
let data = try XMLEncoder().encode(book)
// <Book><title>Swift in Practice</title><author>Apple</author><year>2024</year></Book>Decode
let xml = Data("<Book><title>Swift in Practice</title><author>Apple</author><year>2024</year></Book>".utf8)
let book = try XMLDecoder().decode(Book.self, from: xml)Field Mapping with Macros (Swift 5.9+)
import SwiftXMLCoderMacros
@XMLCodable
struct Product: Codable {
@XMLAttribute var id: String // → attribute
var name: String // → element (default)
var price: Double // → element
}
// <Product id="SKU-1"><name>Widget</name><price>9.99</price></Product>XPath Query
let doc = try XMLDocument(data: xmlData)
let node = try doc.xpathFirstNode("/catalog/book[@lang='en']/title")
print(node?.content ?? "not found")Parser Security
let parser = XMLTreeParser(configuration: .init(
limits: .untrustedInputDefault() // caps depth, nodes, text size
))
let tree = try parser.parse(data: untrustedInput)Streaming — Item-by-Item Codable Decode
struct Product: Decodable { let sku: String; let price: Double }
let products = try XMLItemDecoder().decode(Product.self, itemElement: "Product", from: catalogData)
// Or async, one item at a time (macOS 12+):
for try await product in XMLItemDecoder().items(Product.self, itemElement: "Product", from: catalogData) {
await persist(product)
}Documentation
Guides:
- Getting Started
- Field Mapping
- Namespaces
- Streaming
- Canonicalization
- XPath
- Security
- Swift Version Compatibility
- Test Support
Performance
SwiftXMLCoder ships with a comprehensive benchmark suite covering tree parsing, streaming (SAX push, pull cursor, item-by-item decode), Codable encode/decode, and comparisons against Foundation XMLParser and CoreOffice/XMLCoder.
When to Use What
| Document Size | Recommended Approach | Why | |---------------|---------------------|-----| | < 1 MB | XMLDecoder (tree) | Simplest API, minimal overhead | | 1 - 10 MB | Tree or XMLItemDecoder | Tree works but uses more memory | | > 10 MB | XMLItemDecoder / XMLStreamParser | Constant memory vs linear; tree does not scale |
Measured Results
All figures are p50 wall-clock times. Measured on Apple M1 (arm64, 8 GB), macOS 15.3, release build with jemalloc.
| Operation | 10 KB | 100 KB | 1 MB | 10 MB | |-----------|-------|--------|------|-------| | Tree parse (XMLTreeParser) | 237 µs | 2.3 ms | 24 ms | 232 ms | | SAX push (XMLStreamParser) | 225 µs | 2.2 ms | 20 ms | 208 ms | | Codable decode (XMLDecoder) | 554 µs | 5.6 ms | 55 ms | 545 ms | | Codable encode (XMLEncoder) | 872 µs | 8.2 ms | 81 ms | 829 ms | | Stream write (XMLStreamWriter) | 221 µs | 2.3 ms | 21 ms | 215 ms | | Item decode (XMLItemDecoder, rich model) | — | — | 53 ms | — |
GitHub Actions Results
All figures are p50 wall-clock times from the manual GitHub Actions benchmark run on March 22, 2026, using the macos-15 hosted runner, Swift 6.1, and jemalloc. Run: actions/runs/23411043764.
| Operation | 10 KB | 100 KB | 1 MB | 10 MB | |-----------|-------|--------|------|-------| | Tree parse (XMLTreeParser) | 268 µs | 2.7 ms | 29 ms | 241 ms | | SAX push (XMLStreamParser) | 230 µs | 2.2 ms | 28 ms | 267 ms | | Codable decode (XMLDecoder) | 591 µs | 5.7 ms | 59 ms | 588 ms | | Codable encode (XMLEncoder) | 837 µs | 8.1 ms | 108 ms | 962 ms | | Stream write (XMLStreamWriter) | 243 µs | 2.5 ms | 24 ms | 230 ms | | Item decode (XMLItemDecoder, rich model) | — | — | 74 ms | — |
Cross-Machine Observations
The GitHub-hosted runner preserves the same overall ranking as the local M1 run, but most internal benchmarks are modestly slower in CI. Small-payload encode stays effectively flat, while larger streaming workloads drift more: rich XMLItemDecoder moves from 53 ms to 74 ms at 1 MB. No benchmark job failed, and there were no correctness warnings or crashes in the run.
One important documentation fix came out of the CI comparison: the previous Foundation XMLParser note was inverted. In both local and GitHub Actions measurements, Foundation SAX parsing is faster than SwiftXMLCoder's SAX parser at 1 MB, while SwiftXMLCoder's tree parser remains faster than Foundation XMLDocument.
vs Foundation XMLParser (SAX): SwiftXMLCoder is about 2.0-2.3x slower at 1 MB in both local and GitHub Actions runs (~21-22 ms vs ~9-11 ms). This is consistent across 10 KB and 100 KB as well.
vs Foundation XMLDocument (tree): SwiftXMLCoder is about 18-25% faster at 1 MB (23-24 ms vs 28-32 ms) across local and GitHub Actions runs.
vs CoreOffice/XMLCoder decode: SwiftXMLCoder remains faster on both machines, from about 1.3-1.5x at 100 KB to about 1.5-2.6x at 10 MB depending on the runner.
vs CoreOffice/XMLCoder encode: SwiftXMLCoder remains about 1.5-1.7x faster across all scales on both local and GitHub Actions runs.
Benchmark Coverage
| Area | Scales | What It Measures | |------|--------|-----------------| | Tree parse / decode / encode | 10KB - 10MB | Full DOM materialization + Codable round-trip | | SAX push (XMLStreamParser) | 10KB - 100MB | Event-driven parsing, no tree allocation | | Item-by-item (XMLItemDecoder) | 10KB - 100MB | Streaming Codable decode, constant memory | | Stream writer (XMLStreamWriter) | 10KB - 10MB | Event sequence to XML serialization | | Rich model (nested + attributes) | 10KB - 100MB | Real-world payload with 3-level nesting, namespaces | | Foundation XMLParser comparison | 10KB - 100MB | SAX + tree parse vs Apple's built-in parser | | CoreOffice/XMLCoder comparison | 10KB - 10MB | Codable decode/encode head-to-head |
Running Benchmarks
cd Benchmarks
# Internal benchmarks (parse, stream, encode, decode, Foundation comparison)
swift package --disable-sandbox benchmark
# Comparative benchmarks (SwiftXMLCoder vs CoreOffice/XMLCoder)
swift package --disable-sandbox benchmark --target ComparisonBenchmarksBenchmarks use ordo-one/package-benchmark and require macOS 13+ with jemalloc (brew install jemalloc).
The repository also runs benchmark regression checks in GitHub Actions via .github/workflows/benchmarks.yml. Every PR to main is compared against a main baseline on a macOS runner, so we can track regressions with a shared CI reference instead of relying only on local machine measurements.
Why SwiftXMLCoder?
| Feature | SwiftXMLCoder | CoreOffice/XMLCoder | Foundation XMLParser | |---------|:-:|:-:|:-:| | Codable encode/decode | Yes | Yes | No | | Streaming (constant memory) | Yes | No | Yes (SAX only) | | Item-by-item Codable decode | Yes | No | No | | async/await streaming | Yes | No | No | | XML namespaces | Full | Partial | Yes | | XPath 1.0 queries | Yes | No | No | | Canonicalization (C14N) | Yes | No | No | | @XMLAttribute / @XMLChild macros | Yes (5.9+) | No | No | | Sendable types | All public types | No | No | | Configurable parser limits | Yes | No | No | | Linux support | Yes | Yes | Limited | | Codable encode speed (1 MB) | 81 ms | 133 ms | N/A | | Codable decode speed (1 MB) | 55 ms | 83 ms | N/A |
Requirements
| Component | Requirement | |-----------|-------------| | Swift | 5.6+ (macros require 5.9+) | | macOS | 10.15+ | | iOS | 15.0+ | | tvOS | 15.0+ | | watchOS | 8.0+ | | Linux | Ubuntu 20.04+ with libxml2-dev | | Package manager | Swift Package Manager only |
License
MIT — see LICENSE.
Package Metadata
Repository: mfranceschi6/swift-xml-coder
Default branch: main
README: README.md