XMLDOT is a Go package that provides a fast and simple way to get and set values in XML documents. It has features such as dot notation paths, wildcards, filters, modifiers, and array operations.
Inspired by GJSON and SJSON for JSON.
To start using XMLDOT, install Go and run go get:
$ go get -u github.com/netascode/xmldotThis will retrieve the library.
Experiment with XMLDOT in your browser without installing anything. The playground lets you:
- Test path queries against sample or custom XML
- Explore filters, wildcards, and modifiers
- See results in real-time
- Learn the syntax interactively
Perfect for learning or prototyping queries before using in code.
Get searches XML for the specified path. A path is in dot syntax, such as "book.title" or "book.@id". When the value is found it's returned immediately.
package main
import "github.com/netascode/xmldot"
const xml = `
<catalog>
<book id="1">
<title>The Go Programming Language</title>
<author>Alan Donovan</author>
<price>44.99</price>
</book>
</catalog>`
func main() {
title := xmldot.Get(xml, "catalog.book.title")
println(title.String())
}This will print:
The Go Programming Language
The fluent API enables method chaining on Result objects for cleaner, more readable code:
// Basic fluent chaining
root := xmldot.Get(xml, "root")
name := root.Get("user.name").String()
age := root.Get("user.age").Int()
// Deep chaining
fullPath := xmldot.Get(xml, "root").
Get("company").
Get("department").
Get("team.member").
Get("name").
String()
// Batch queries
user := xmldot.Get(xml, "root.user")
results := user.GetMany("name", "age", "email")
name := results[0].String()
age := results[1].Int()
email := results[2].String()
// Case-insensitive queries
opts := &xmldot.Options{CaseSensitive: false}
name := root.GetWithOptions("USER.NAME", opts).String()
// Structure inspection with Map()
user := xmldot.Get(xml, "root.user")
m := user.Map()
name := m["name"].String()
age := m["age"].Int()
// Iterate over all child elements
for key, value := range m {
fmt.Printf("%s = %s\n", key, value.String())
}
// Map() with case-insensitive keys
m := user.MapWithOptions(&xmldot.Options{CaseSensitive: false})Performance: Fluent chaining adds ~280% overhead for 3-level chains compared to full paths. For performance-critical code, use direct paths:
// Fast (recommended for hot paths)
name := xmldot.Get(xml, "root.user.name")
// Readable (recommended for business logic)
user := xmldot.Get(xml, "root.user")
name := user.Get("name")Array Handling: Field extraction on arrays requires explicit #.field syntax:
items := xmldot.Get(xml, "catalog.items")
// Extract all prices
prices := items.Get("item.#.price") // Array of all pricesSet modifies an XML value for the specified path. A path is in dot syntax, such as "book.title" or "book.@id".
package main
import "github.com/netascode/xmldot"
const xml = `
<catalog>
<book id="1">
<title>The Go Programming Language</title>
<price>44.99</price>
</book>
</catalog>`
func main() {
value, _ := xmldot.Set(xml, "catalog.book.price", 39.99)
println(value)
}This will print:
<catalog><book id="1"><title>The Go Programming Language</title><price>39.99</price></book></catalog>Set automatically creates missing parent elements when setting attributes. This makes it easy to add attributes to elements that don't yet exist:
xml := `<root></root>`
// Automatically creates <user> element with id attribute
result, _ := xmldot.Set(xml, "root.user.@id", "123")
// Result: <root><user id="123"></user></root>
// Works with deep paths too
result, _ = xmldot.Set(xml, "root.company.department.@name", "Engineering")
// Result: <root><company><department name="Engineering"></department></company></root>A path is a series of keys separated by a dot. The dot character can be escaped with \.
<catalog>
<book id="1">
<title>The Go Programming Language</title>
<author>Alan Donovan</author>
<price>44.99</price>
<tags>
<tag>programming</tag>
<tag>go</tag>
</tags>
</book>
<book id="2">
<title>Learning Go</title>
<author>Jon Bodner</author>
<price>39.99</price>
</book>
</catalog>catalog.book.title >> "The Go Programming Language"
catalog.book.@id >> "1"
catalog.book.price >> "44.99"
catalog.book.1.title >> "Learning Go"
catalog.book.# >> 2
catalog.book.tags.tag.0 >> "programming"
catalog.book.title.% >> "The Go Programming Language"
Array elements are accessed by index:
catalog.book.0.title >> "The Go Programming Language" (first book)
catalog.book.1.title >> "Learning Go" (second book)
catalog.book.# >> 2 (count of books)
catalog.book.tags.tag.# >> 2 (count of tags)
Append new elements using index -1 with Set() or SetRaw():
xml := `<catalog><book><title>Book 1</title></book></catalog>`
// Append a new book using SetRaw for XML content
result, _ := xmldot.SetRaw(xml, "catalog.book.-1", "<title>Book 2</title>")
count := xmldot.Get(result, "catalog.book.#")
// count.Int() → 2
// Works with empty arrays too
xml2 := `<catalog></catalog>`
result2, _ := xmldot.SetRaw(xml2, "catalog.book.-1", "<title>First Book</title>")
// Result: <catalog><book><title>First Book</title></book></catalog>Attributes are accessed with the @ prefix:
catalog.book.@id >> "1"
catalog.book.0.@id >> "1"
catalog.book.1.@id >> "2"
Text content (ignoring child elements) uses the % operator:
catalog.book.title.% >> "The Go Programming Language"
Single-level wildcards * match any element at that level. Recursive wildcards ** match elements at any depth:
<catalog>
<book id="1">
<title>The Go Programming Language</title>
<price>44.99</price>
</book>
<book id="2">
<title>Learning Go</title>
<price>39.99</price>
</book>
</catalog>catalog.*.title >> ["The Go Programming Language", "Learning Go"]
catalog.book.*.% >> ["The Go Programming Language", "Alan Donovan", "44.99", ...]
catalog.**.price >> ["44.99", "39.99"] (all prices at any depth)
You can filter elements using GJSON-style query syntax. Supports ==, !=, <, >, <=, >=, %, !% operators:
<catalog>
<book status="active">
<title>The Go Programming Language</title>
<price>44.99</price>
</book>
<book status="active">
<title>Learning Go</title>
<price>39.99</price>
</book>
<book status="discontinued">
<title>Old Book</title>
<price>19.99</price>
</book>
</catalog>catalog.book.#(price>40).title >> "The Go Programming Language"
catalog.book.#(@status==active)#.title >> ["The Go...", "Learning Go"]
catalog.book.#(price<30).#(@status==active) >> [] (no matches)
catalog.book.#(title%"*Go*")#.title >> ["The Go...", "Learning Go"] (pattern match)
Modifiers transform query results using the | operator:
catalog.book.title|@reverse >> ["Learning Go", "The Go..."]
catalog.book.price|@sort >> ["39.99", "44.99"]
catalog.book.title|@first >> "The Go Programming Language"
catalog.book.title|@last >> "Learning Go"
catalog.book|@pretty >> formatted XML
@reverse: Reverse array order@sort: Sort array elements@first: Get first element@last: Get last element@keys: Get element names@values: Get element values@flatten: Flatten nested arrays@pretty: Format XML with indentation@ugly: Remove all whitespace@raw: Get raw XML without parsing
You can add your own modifiers:
xmldot.AddModifier("uppercase", func(xml, arg string) string {
return strings.ToUpper(xml)
})
result := xmldot.Get(xml, "catalog.book.title|@uppercase")
// "THE GO PROGRAMMING LANGUAGE"The Result.Array() function returns an array of values. The ForEach function allows iteration:
result := xmldot.Get(xml, "catalog.book.title")
for _, title := range result.Array() {
println(title.String())
}Or use ForEach:
xmldot.Get(xml, "catalog.book").ForEach(func(_, book xmldot.Result) bool {
println(book.Get("title").String())
return true // keep iterating
})XMLDOT returns a Result type that holds the value and provides methods to access it:
result.Type // String, Number, True, False, Null, or XML
result.Str // the string value
result.Num // the float64 number
result.Raw // the raw xml
result.Index // index in original xml
result.String() string
result.Bool() bool
result.Int() int64
result.Float() float64
result.Array() []Result
result.Exists() bool
result.IsArray() bool
result.Value() interface{}
result.Get(path string) Result
result.GetMany(paths ...string) []Result
result.GetWithOptions(path string, opts *Options) Result
result.ForEach(iterator func(index int, value Result) bool)Basic namespace prefix matching is supported:
<root xmlns:ns="http://example.com">
<ns:item>value</ns:item>
</root>root.ns:item >> "value"
Note: Only prefix matching is supported. Namespace URIs are not resolved. For full namespace support, use encoding/xml.
Validate XML before processing:
if !xmldot.Valid(xml) {
return errors.New("invalid xml")
}
// Or get detailed errors
if err := xmldot.ValidateWithError(xml); err != nil {
fmt.Printf("Error at line %d, column %d: %s\n",
err.Line, err.Column, err.Message)
}xmldot supports XML fragments with multiple root elements. Fragments with matching root names can be treated as arrays:
fragment := `<user id="1"><name>Alice</name></user>
<user id="2"><name>Bob</name></user>
<user id="3"><name>Carol</name></user>`
// Validation accepts multiple roots
if xmldot.Valid(fragment) {
fmt.Println("Fragment is valid")
}
// Query first matching root
name := xmldot.Get(fragment, "user.name") // → "Alice"
// Array operations on matching roots
count := xmldot.Get(fragment, "user.#") // → 3
first := xmldot.Get(fragment, "user.0.name") // → "Alice"
names := xmldot.Get(fragment, "user.#.name") // → ["Alice", "Bob", "Carol"]
// Modify first matching root
result, _ := xmldot.Set(fragment, "user.@status", "active")
// Build fragments incrementally using root-level append
xml := `<user>Alice</user>`
xml, _ = xmldot.Set(xml, "item.-1", "first") // Creates sibling: <user>Alice</user><item>first</item>
xml, _ = xmldot.Set(xml, "item.-1", "second") // Appends sibling: <user>Alice</user><item>first</item><item>second</item>Get multiple paths efficiently:
results := xmldot.GetMany(xml, "catalog.book.0.title", "catalog.book.0.price")
println(results[0].String()) // title
println(results[1].Float()) // priceSet multiple paths:
paths := []string{"catalog.book.0.price", "catalog.book.1.price"}
values := []interface{}{39.99, 34.99}
result, _ := xmldot.SetMany(xml, paths, values)Delete multiple paths:
result, _ := xmldot.DeleteMany(xml, "catalog.book.0.tags", "catalog.book.1.tags")If your XML is in a []byte slice, use GetBytes:
var xml []byte = ...
result := xmldot.GetBytes(xml, "catalog.book.title")Zero External Dependencies: XMLDOT uses only Go standard library for portability and security. All functionality including pattern matching uses internal implementations with built-in security protections.
All read operations are thread-safe and can be used concurrently without synchronization:
// Safe: concurrent reads
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
result := xmldot.Get(xml, fmt.Sprintf("users.user.%d.name", id))
process(result)
}(i)
}
wg.Wait()Write operations require external synchronization:
var mu sync.Mutex
currentXML := "<root></root>"
func updateXML(path string, value interface{}) {
mu.Lock()
defer mu.Unlock()
result, _ := xmldot.Set(currentXML, path, value)
currentXML = result
}See the Concurrency Guide for patterns and best practices.
- Path Syntax Reference - Complete path expression guide
- Error Handling Guide - Error types and patterns
- Performance Guide - Optimization techniques
- Concurrency Guide - Thread-safety patterns
- Security Guide - Security features and limits
- Migration Guide - Moving from other libraries
- Basic Get Queries - Simple element access
- Basic Set Operations - Modify XML documents
- Array Manipulation - Working with arrays
- Query Filters - Filter elements by conditions
- Result Modifiers - Transform query results
- Custom Modifiers - Build custom transformations
- Namespace Support - Work with XML namespaces
- Performance - Optimize for speed
- RSS Parser - Parse RSS/Atom feeds
- Config Files - Manage XML config files
- SOAP Client - Build SOAP clients
Full API reference available at GoDoc.
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
This project is licensed under the MIT License - see the LICENSE file for details.
Inspired by the excellent gjson and sjson libraries for JSON.