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
letyamlString="""name: John Doeage: 30hobbies: - reading - swimming - coding"""do{letnode=tryYAML.parse(yamlString)
// Access values
letname=node["name"]?.string // "John Doe"
letage=node["age"]?.int // 30
lethobbies=node["hobbies"]?.array?.compactMap{ $0.string } // ["reading", "swimming", "coding"]
}catch{print("Error parsing YAML: \(error)")}import YAML
letnode=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"))])])letyamlString=YAML.emit(node)print(yamlString)
// Output:
// name: Jane Smith
// age: 25
// hobbies:
// - painting
// - travelingimport YAML
structPerson:Codable{letname:Stringletage:Intletemail:String?}
// Encoding
letperson=Person(name:"Alice", age:28, email:"alice@example.com")letencoder=YAMLEncoder()letyamlString=try encoder.encode(person)
// Decoding
letdecoder=YAMLDecoder()letdecoded=try decoder.decode(Person.self, from: yamlString)For embedded systems and platforms without Foundation/Codable support:
import YAML
// Build YAML using the lightweight API
letyamlNode=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
letyamlString=YAMLBuilder.build(from: yamlNode)
// Access values using path notation
lettemp= yamlNode.value(at:"temperature")?.double // 23.5
letfirstReading= yamlNode.value(at:"readings.0")?.int // 100
// Use result builders for cleaner syntax
letdocument=yaml{YAMLNode.dictionary(["version":.string("1.0"),"sensors":.array([.dictionary(["id":.string("temp-1"),"value":.double(22.8)])])])}// Parse YAML with version directive
letyaml="""%YAML 1.2---name: test"""letnode=tryYAML.parse(yaml)// Use merge keys to inherit mappings
letyaml="""defaults: &defaults timeout: 30 retries: 3development: <<: *defaults host: localhostproduction: <<: *defaults host: production.example.com timeout: 60 # Override default"""letconfig=tryYAML.parse(yaml)
// production.timeout will be 60, not 30// Parse multiple documents
letmultiDoc="""---document: first---document: second"""letdocuments=tryYAML.parseAll(multiDoc)print(documents.count) // 2
// Emit multiple documents
letyaml=YAML.emitAll([node1, node2])letnode=YAMLNode.scalar(.init(
value:"This is a long text that spans multiple lines",
style:.literal // Will use | style
))varoptions=YAMLEmitter.Options()
options.useFlowStyle =trueletyaml=YAML.emit(node, options: options)// Decoding with snake_case to camelCase conversion
vardecoderOptions=YAMLDecoder.Options()
decoderOptions.keyDecodingStrategy =.convertFromSnakeCase
letdecoder=YAMLDecoder(options: decoderOptions)
// Encoding with camelCase to snake_case conversion
varencoderOptions=YAMLEncoder.Options()
encoderOptions.keyEncodingStrategy =.convertToSnakeCase
letencoder=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
letparser=YAMLStreamParser()
// Implement delegate to receive parsing events
classMyDelegate: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}}}letdelegate=MyDelegate()
parser.delegate = delegate
// Parse a large file
try parser.parse(contentsOf: largeFileURL)// Process only top-level entries of a large YAML file
tryYAMLStreamParser.processTopLevel(of: fileURL){ key, value inprint("Top-level entry: \(key) = \(value)")}
// Filter specific keys
tryYAMLStreamParser.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
letparser=YAMLStreamParser()letbuilder=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
letinputStream=InputStream(url: fileURL)!
letparser=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:
publicenumYAMLNode{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
letnode=tryYAML.parse(yamlString)
// Emit YAML string
letyamlString=YAML.emit(node, options: options)Codable support for encoding and decoding Swift types:
letencoder=YAMLEncoder()letyaml=try encoder.encode(value)letdecoder=YAMLDecoder()letvalue=try decoder.decode(Type.self, from: yaml)Token-based streaming parser for processing large YAML files:
letparser=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:
publicenumYAMLToken{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:
letbuilder=YAMLStreamBuilder()
builder.maxDepth =3 // Limit building depth
builder.onNodeComplete ={ node in
// Handle completed node
}The library provides detailed error messages:
publicenumYAMLError: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 valueSupporting complex keys would require changing from hash-based lookups O(1) to linear searches O(n):
// Current fast API with string keys:
letvalue=node["config"]?["timeout"] // O(1) lookup
// With complex keys - much slower:
letvalue= node.findValue{ key, _ in
key ==YAMLNode.sequence([.scalar("a"),.scalar("b")])} // O(n) searchString 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"]?.stringIf 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: config1environments:
- type: dbenv: prodconfig: config2This 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.