A robust YAML parser and manipulator for Swift 6.0 and higher, with full support for YAML 1.2 specification.
- π Swift 6.0+ Support - Built with the latest Swift features and concurrency safety
- π± Multi-Platform - Supports iOS, macOS, visionOS, tvOS, watchOS, Linux, Windows, and Android
- π Codable Support - Seamlessly encode and decode Swift types to/from YAML
- π― Type Safe - Strongly typed YAML nodes with convenient accessors
- π Full YAML 1.2 Support - Including anchors, aliases, merge keys, and complex data structures
- β‘ High Performance - Optimized for speed and memory efficiency with streaming capabilities
- π‘οΈ Safe - Comprehensive error handling with detailed error messages
- π Merge Keys - Full support for YAML merge key (
<<
) functionality - π Multi-Document - Parse and emit multiple YAML documents in a single stream
- π·οΈ Custom Tags - Support for YAML tags and type annotations
- π Flexible Indentation - Compliant with YAML spec's flexible indentation rules
- πΎ Embedded Swift - Non-Codable API for embedded systems
Add the following to your Package.swift
file:
dependencies: [
.package(url: "https://github.com/edgeengineer/yaml.git", from: "0.0.1")
]
Then add YAML
to your target dependencies:
.target(
name: "YourTarget",
dependencies: ["YAML"]
)
import YAML
let yamlString = """
name: John Doe
age: 30
hobbies:
- reading
- swimming
- coding
"""
do {
let node = try YAML.parse(yamlString)
// Access values
let name = node["name"]?.string // "John Doe"
let age = node["age"]?.int // 30
let hobbies = node["hobbies"]?.array?.compactMap { $0.string } // ["reading", "swimming", "coding"]
} catch {
print("Error parsing YAML: \(error)")
}
import YAML
let node = YAMLNode.mapping([
"name": .scalar(.init(value: "Jane Smith")),
"age": .scalar(.init(value: "25", tag: .int)),
"hobbies": .sequence([
.scalar(.init(value: "painting")),
.scalar(.init(value: "traveling"))
])
])
let yamlString = YAML.emit(node)
print(yamlString)
// Output:
// name: Jane Smith
// age: 25
// hobbies:
// - painting
// - traveling
import YAML
struct Person: Codable {
let name: String
let age: Int
let email: String?
}
// Encoding
let person = Person(name: "Alice", age: 28, email: "[email protected]")
let encoder = YAMLEncoder()
let yamlString = try encoder.encode(person)
// Decoding
let decoder = YAMLDecoder()
let decoded = try decoder.decode(Person.self, from: yamlString)
For embedded systems and platforms without Foundation/Codable support:
import YAML
// Build YAML using the lightweight API
let yamlNode = YAMLNode.dictionary([
"device": .string("sensor-001"),
"temperature": .double(23.5),
"active": .bool(true),
"readings": .array([
.int(100),
.int(102),
.int(98)
])
])
// Convert to YAML string
let yamlString = YAMLBuilder.build(from: yamlNode)
// Access values using path notation
let temp = yamlNode.value(at: "temperature")?.double // 23.5
let firstReading = yamlNode.value(at: "readings.0")?.int // 100
// Use result builders for cleaner syntax
let document = yaml {
YAMLNode.dictionary([
"version": .string("1.0"),
"sensors": .array([
.dictionary([
"id": .string("temp-1"),
"value": .double(22.8)
])
])
])
}
// Parse YAML with version directive
let yaml = """
%YAML 1.2
---
name: test
"""
let node = try YAML.parse(yaml)
// Use merge keys to inherit mappings
let yaml = """
defaults: &defaults
timeout: 30
retries: 3
development:
<<: *defaults
host: localhost
production:
<<: *defaults
host: production.example.com
timeout: 60 # Override default
"""
let config = try YAML.parse(yaml)
// production.timeout will be 60, not 30
// Parse multiple documents
let multiDoc = """
---
document: first
---
document: second
"""
let documents = try YAML.parseAll(multiDoc)
print(documents.count) // 2
// Emit multiple documents
let yaml = YAML.emitAll([node1, node2])
let node = YAMLNode.scalar(.init(
value: "This is a long text that spans multiple lines",
style: .literal // Will use | style
))
var options = YAMLEmitter.Options()
options.useFlowStyle = true
let yaml = YAML.emit(node, options: options)
// Decoding with snake_case to camelCase conversion
var decoderOptions = YAMLDecoder.Options()
decoderOptions.keyDecodingStrategy = .convertFromSnakeCase
let decoder = YAMLDecoder(options: decoderOptions)
// Encoding with camelCase to snake_case conversion
var encoderOptions = YAMLEncoder.Options()
encoderOptions.keyEncodingStrategy = .convertToSnakeCase
let encoder = YAMLEncoder(options: encoderOptions)
For processing large YAML files without loading the entire document into memory, use the streaming API:
import YAML
// Create a streaming parser
let parser = YAMLStreamParser()
// Implement delegate to receive parsing events
class MyDelegate: YAMLStreamParserDelegate {
func parser(_ parser: YAMLStreamParser, didParse token: YAMLToken) {
switch token {
case .key(let key):
print("Found key: \(key)")
case .scalar(let scalar):
print("Found value: \(scalar.value)")
case .mappingStart:
print("Starting mapping")
case .sequenceStart:
print("Starting sequence")
default:
break
}
}
}
let delegate = MyDelegate()
parser.delegate = delegate
// Parse a large file
try parser.parse(contentsOf: largeFileURL)
// Process only top-level entries of a large YAML file
try YAMLStreamParser.processTopLevel(of: fileURL) { key, value in
print("Top-level entry: \(key) = \(value)")
}
// Filter specific keys
try YAMLStreamParser.processTopLevel(of: fileURL, keys: ["metadata", "config"]) { key, value in
// Only receives entries for "metadata" and "config" keys
print("\(key): \(value)")
}
// Build complete YAML nodes from stream
let parser = YAMLStreamParser()
let builder = YAMLStreamBuilder()
builder.onNodeComplete = { node in
// Process each complete node
print("Complete node: \(node)")
}
parser.delegate = builder
try parser.parse(yaml)
// Limit depth for memory efficiency
builder.maxDepth = 2 // Only build nodes up to depth 2
// Parse from any InputStream
let inputStream = InputStream(url: fileURL)!
let parser = YAMLStreamParser()
parser.delegate = myDelegate
try parser.parse(from: inputStream)
The streaming API is ideal for:
- π Processing large data files (logs, datasets, configurations)
- π Extracting specific information without full parsing
- πΎ Memory-constrained environments
- π Real-time YAML processing
The core data structure representing YAML content:
public enum YAMLNode {
case scalar(Scalar)
case sequence([YAMLNode])
case mapping([String: YAMLNode])
}
With convenient accessors:
.string
- Get string value.int
- Get integer value.double
- Get double value.bool
- Get boolean value.array
- Get array of nodes.dictionary
- Get dictionary of nodes[index]
- Subscript for sequences[key]
- Subscript for mappings
Main entry point for parsing and emitting:
// Parse YAML string
let node = try YAML.parse(yamlString)
// Emit YAML string
let yamlString = YAML.emit(node, options: options)
Codable support for encoding and decoding Swift types:
let encoder = YAMLEncoder()
let yaml = try encoder.encode(value)
let decoder = YAMLDecoder()
let value = try decoder.decode(Type.self, from: yaml)
Token-based streaming parser for processing large YAML files:
let parser = YAMLStreamParser()
parser.delegate = myDelegate
// Parse from string
try parser.parse(yamlString)
// Parse from file
try parser.parse(contentsOf: fileURL)
// Parse from input stream
try parser.parse(from: inputStream)
Events emitted by the streaming parser:
public enum YAMLToken {
case documentStart
case documentEnd
case mappingStart
case mappingEnd
case sequenceStart
case sequenceEnd
case key(String)
case scalar(YAMLNode.Scalar)
}
Builds YAML nodes from streaming tokens:
let builder = YAMLStreamBuilder()
builder.maxDepth = 3 // Limit building depth
builder.onNodeComplete = { node in
// Handle completed node
}
The library provides detailed error messages:
public enum YAMLError: Error, LocalizedError {
case invalidYAML(String)
case unexpectedToken(String, line: Int, column: Int)
case indentationError(String, line: Int)
case unclosedQuote(line: Int)
case invalidEscape(String, line: Int)
}
While the YAML specification allows sequences and mappings to be used as keys, this library intentionally only supports string keys. Here's why:
Complex keys are virtually never used in real-world YAML files. After analyzing thousands of YAML configurations across various domains (Kubernetes, Docker, CI/CD pipelines, application configs), we found zero instances of complex keys being used.
# Never seen in practice:
? [a, b, c]
: some value
? {name: test}
: another value
# What everyone actually uses:
simple_key: value
"quoted key": another value
Supporting complex keys would require changing from hash-based lookups O(1)
to linear searches O(n)
:
// Current fast API with string keys:
let value = node["config"]?["timeout"] // O(1) lookup
// With complex keys - much slower:
let value = node.findValue { key, _ in
key == YAMLNode.sequence([.scalar("a"), .scalar("b")])
} // O(n) search
String keys enable a clean, intuitive API that matches developer expectations:
// Clean and simple:
config["database"]["host"]?.string
// vs complex key API:
config.mapping?.first { (key, value) in
key.dictionary?["type"]?.string == "database"
}?.value.dictionary?["host"]?.string
If you absolutely need complex key-like behavior, use string representations:
# Instead of complex keys:
"[prod, us-east]": config1
"{type: db, env: prod}": config2
# Or use nested structures:
regions:
prod:
us-east: config1
environments:
- type: db
env: prod
config: config2
This design decision prioritizes real-world usage patterns, performance, and API ergonomics over spec completeness.
- Swift 6.0+
- Xcode 16.0+ (for Apple platforms)
This library is released under the Apache 2.0 License. See LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.