Skip to content

MFranceschi6/swift-xml-coder

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

56 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

SwiftXMLCoder

CI Lint codecov License: MIT

A high-performance, Codable-compatible XML encoder and decoder for Swift, backed by libxml2.

Encode and decode any Codable type to XML with streaming support, element vs. attribute mapping, namespace declarations, date strategies, XPath queries, and deterministic canonicalization — all with zero reflection, full Sendable safety, and constant-memory streaming for large documents.


Features

  • XMLEncoder / XMLDecoder — Codable-compatible, zero-reflection encoding and decoding
  • Three-tier field mapping@XMLAttribute / @XMLChild property wrappers, @XMLCodable macros (Swift 5.9+), or runtime XMLFieldCodingOverrides
  • StreamingXMLStreamParser (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 diagnosticsXMLParsingError.decodeFailed with coding path and XMLSourceLocation for precise error reporting
  • Immutable tree model — value-semantic XMLTreeDocument for transform pipelines; full fidelity for processing instructions, doctypes, and comments
  • Swift 5.6 – 6.1 — multi-manifest compatibility; macros on 5.9+; ~Copyable ownership 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

Full API Reference (DocC)

Guides:


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 ComparisonBenchmarks

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

About

High-performance Codable XML encoder/decoder for Swift — streaming, canonicalization, macros, namespaces. Backed by libxml2. SPM-only, Linux-compatible.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages