Help us shape the future of Go! Take the 2026 Go Developer Survey

xmldot

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 11 Imported by: 1

README

xmldot logo

Latest Version
GoDoc Go Report Card codecov Playground

get and set xml values with dot notation

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.

Getting Started

Installing

To start using XMLDOT, install Go and run go get:

$ go get -u github.com/netascode/xmldot

This will retrieve the library.

Try It Online

🎮 Interactive Playground

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 a value

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

Fluent API

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 prices

Set a value

Set 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>
Creating Elements with Attributes

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>

Path Syntax

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"
Arrays

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

Attributes are accessed with the @ prefix:

catalog.book.@id             >> "1"
catalog.book.0.@id           >> "1"
catalog.book.1.@id           >> "2"
Text Content

Text content (ignoring child elements) uses the % operator:

catalog.book.title.%         >> "The Go Programming Language"

Wildcards

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)

Filters

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

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
Built-in modifiers
  • @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
Custom modifiers

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"

Working with Arrays

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
})

Result Type

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)

Namespaces

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.

Validation

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)
}

XML Fragments (Multiple Roots)

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>

Multiple Paths

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())   // price

Set 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")

Working with Bytes

If your XML is in a []byte slice, use GetBytes:

var xml []byte = ...
result := xmldot.GetBytes(xml, "catalog.book.title")

Design Philosophy

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.

Concurrency

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.

Documentation

Guides
Examples
Real-World Examples
API Documentation

Full API reference available at GoDoc.

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

Inspired by the excellent gjson and sjson libraries for JSON.

Documentation

Overview

Package xmldot provides high-performance XML querying and manipulation using GJSON-inspired path syntax.

xmldot brings the simplicity and power of GJSON's dot-notation paths to XML, enabling intuitive queries and modifications with built-in security protections.

Features

  • Simple dot-notation paths: "root.user.name"
  • Array operations: indexing, counting, filtering
  • GJSON-style queries: #(condition) and #(condition)# filters
  • Field extraction: #.field to extract fields from all array elements
  • Wildcards: * (single-level) and ** (recursive)
  • Modifiers: @reverse, @sort, @first, @last, @flatten, etc.
  • Built-in security: DoS protection, resource limits, XXE prevention
  • Zero dependencies: uses only Go standard library
  • High performance: 400-1000ns/op for basic queries

Basic Usage

Query XML documents using intuitive path syntax:

xml := `<root><user><name>John</name><age>30</age></user></root>`
result := xmldot.Get(xml, "root.user.name")
fmt.Println(result.String()) // "John"

Access attributes using @ prefix:

xml := `<user id="123"><name>John</name></user>`
id := xmldot.Get(xml, "user.@id")
fmt.Println(id.String()) // "123"

Work with arrays using indexing and counting:

xml := `<users><user>Alice</user><user>Bob</user></users>`
count := xmldot.Get(xml, "users.user.#")
fmt.Println(count.Int()) // 2
first := xmldot.Get(xml, "users.user.0")
fmt.Println(first.String()) // "Alice"

Filter arrays using GJSON-style queries:

xml := `<users>
  <user><name>Alice</name><age>30</age></user>
  <user><name>Bob</name><age>25</age></user>
</users>`
result := xmldot.Get(xml, "users.user.#(age>28)#.name")
fmt.Println(result.Array()[0].String()) // "Alice"

Modify XML documents:

xml := `<root><value>old</value></root>`
updated, _ := xmldot.Set(xml, "root.value", "new")
result := xmldot.Get(updated, "root.value")
fmt.Println(result.String()) // "new"

Security

This package implements multiple security protections to prevent common XML vulnerabilities and denial-of-service attacks:

  • Document and value size limits (MaxDocumentSize, MaxValueSize: 100 MB)
  • Nesting depth limits (MaxNestingDepth: 100 levels)
  • Attribute limits (MaxAttributes: 1000 per element)
  • Token size limits (MaxTokenSize: 1 MB)
  • Array and recursive wildcard result limits (MaxArrayResults, MaxWildcardResults: 100000)
  • CPU exhaustion protection (MaxRecursiveOperations, MaxPatternIterations)
  • Filter and path limits (MaxFilterExpressionLength, MaxPathSegments)
  • XXE attack prevention (DOCTYPE declarations skipped)
  • No entity expansion (entities not processed)

Exceeding a limit fails the operation instead of returning a partial result: Get returns a Null Result whose Err field holds a *LimitError, and Set and Delete return the *LimitError. Use errors.Is(err, ErrLimitExceeded) to detect limit violations. The limits are package variables that applications processing trusted documents can adjust once during initialization.

Path Syntax

The path syntax is inspired by GJSON and supports:

  • Dot notation: "root.child.element"
  • Attributes: "element.@attribute"
  • Array indexing: "items.item.0" (first), "items.item.-1" (last)
  • Array counting: "items.item.#"
  • Text content: "element.%" (direct text only)
  • Wildcards: "root.*.name" (any child), "root.**.price" (any depth)
  • Filters: "items.item.#(price>10)#.name"
  • Field extraction: "items.item.#.name" (extract name from all items)
  • Modifiers: "items.item.#.price|@sort|@reverse"

For complete path syntax documentation, see docs/path-syntax.md

Performance

xmldot is designed for high performance with typical operations completing in sub-microsecond time:

  • Simple paths: 200-500ns
  • Path with caching: ~200ns
  • Filter queries: 1-2µs
  • Recursive wildcards: 2-5µs

The parser uses an incremental parsing approach that stops as soon as the requested path is found, avoiding unnecessary parsing of large documents.

Concurrency

All query operations (Get, GetMany, GetWithOptions) are safe for concurrent use from multiple goroutines. Each call creates its own parser instance with no shared state.

Modification operations (Set, Delete) return new XML strings and are also safe for concurrent use, though modifications to the same document should be coordinated by the caller to avoid lost updates.

For More Information

Complete documentation: https://github.com/netascode/xmldot

Documentation files:

  • docs/path-syntax.md - Complete path syntax reference
  • docs/performance.md - Performance guide and benchmarks
  • docs/security.md - Security features and best practices
  • docs/error-handling.md - Error handling patterns
  • docs/migration.md - Migration from other XML libraries

Examples: See the examples/ directory for runnable code samples.

Example (AttributeFilter)

Example_attributeFilter demonstrates filtering elements by attribute value

xml := `<items>
		<item id="1" status="active"><title>First</title></item>
		<item id="2" status="inactive"><title>Second</title></item>
		<item id="3" status="active"><title>Third</title></item>
	</items>`

// Get titles of active items
result := Get(xml, "items.item.#(@status==active)#.title")
for _, r := range result.Array() {
	fmt.Println(r.String())
}
Output:
First
Third
Example (NumericFilter)

Example_numericFilter demonstrates filtering elements by numeric comparison

xml := `<users>
		<user><name>Alice</name><age>25</age></user>
		<user><name>Bob</name><age>18</age></user>
		<user><name>Carol</name><age>30</age></user>
	</users>`

// Get users older than 21
result := Get(xml, "users.user.#(age>21)#.name")
for _, r := range result.Array() {
	fmt.Println(r.String())
}
Output:
Alice
Carol
Example (RecursiveWildcard)

Example_recursiveWildcard demonstrates using recursive wildcards

xml := `<catalog>
		<product><price>10</price></product>
		<category>
			<product><price>20</price></product>
			<subcategory>
				<product><price>30</price></product>
			</subcategory>
		</category>
	</catalog>`

// Find all price elements at any depth
result := Get(xml, "catalog.**.price")
for _, r := range result.Array() {
	fmt.Println(r.String())
}
Output:
10
20
30
Example (SingleLevelWildcard)

Example_singleLevelWildcard demonstrates using single-level wildcards

xml := `<root>
		<user><name>Alice</name></user>
		<admin><name>Bob</name></admin>
		<guest><name>Carol</name></guest>
	</root>`

// Get all names from direct children of root
result := Get(xml, "root.*.name")
for _, r := range result.Array() {
	fmt.Println(r.String())
}
Output:
Alice
Bob
Carol

Index

Examples

Constants

View Source
const (
	// MaxPositionOffset prevents position arithmetic overflow in deeply nested documents
	// Set to ~1GB max to prevent overflow while allowing large documents
	MaxPositionOffset = 1<<30 - 1
)

Variables

View Source
var (
	// ErrInvalidPath is returned when the path syntax is invalid or unsupported.
	ErrInvalidPath = errors.New("invalid path syntax")

	// ErrMalformedXML is returned when the input XML document is malformed
	// or cannot be parsed.
	ErrMalformedXML = errors.New("malformed XML document")

	// ErrInvalidValue is returned when the value cannot be converted to XML
	// or is inappropriate for the operation.
	ErrInvalidValue = errors.New("invalid value for XML")
)
View Source
var (
	// MaxDocumentSize is the maximum allowed size for an XML document in bytes.
	MaxDocumentSize = 100 * 1024 * 1024

	// MaxValueSize is the maximum allowed size in bytes for a single value in Set operations.
	MaxValueSize = 100 * 1024 * 1024

	// MaxNestingDepth is the maximum allowed nesting depth for XML elements.
	// This prevents stack exhaustion with deeply nested structures.
	MaxNestingDepth = 100

	// MaxAttributes is the maximum number of attributes allowed per element.
	MaxAttributes = 1000

	// MaxTokenSize is the maximum size in bytes for a single element name, attribute
	// name, attribute value, closing tag or processing instruction.
	MaxTokenSize = 1024 * 1024

	// MaxNamespacePrefixLength is the maximum allowed length for namespace prefixes in paths.
	MaxNamespacePrefixLength = 256

	// MaxArrayResults is the maximum number of sibling elements collected by
	// non-recursive array operations: count (#), index (.N), field extraction (#.field),
	// filters, single-level wildcards, fragment roots and Map(). These scan a single
	// level of the document, so their cost is linear and already bounded by
	// MaxDocumentSize. The limit only guards against per-match memory overhead.
	MaxArrayResults = 100000

	// MaxWildcardResults is the maximum number of results from recursive wildcard
	// queries ("**").
	MaxWildcardResults = 100000

	// MaxRecursiveOperations is the minimum operation budget for a recursive wildcard
	// query ("**"). The effective budget is the larger of this value and the size of
	// the searched document in bytes, so a single recursive search over any document
	// always completes, while chained recursive searches cannot grow without bound.
	MaxRecursiveOperations = 10000

	// MaxFilterExpressionLength is the maximum allowed length for filter expressions.
	MaxFilterExpressionLength = 4096

	// MaxPatternIterations is the step budget for a single pattern match (% and !%
	// filter operators), on top of one step per byte of the matched value. Typical
	// patterns match in linear time and always complete; only pathological
	// value/pattern combinations, which take up to len(value)*len(pattern) steps,
	// can exceed it.
	MaxPatternIterations = 10000000

	// MaxModifierChainDepth is the maximum number of chained modifiers in a path.
	MaxModifierChainDepth = 20

	// MaxPathSegments is the maximum number of path segments allowed in a query path.
	MaxPathSegments = 100

	// MaxFieldNameLength is the maximum length of a field name in #.field syntax.
	MaxFieldNameLength = 256
)

Security limits to protect against malicious XML and pathological queries.

The limits are variables so that applications processing trusted documents can adjust them. They are read without synchronization, so set them once during program initialization, before any concurrent use of the package.

When a limit is exceeded, the operation fails instead of returning a partial result: Get and related functions return a Null Result whose Err field holds a *LimitError, and Set, Delete and related functions return the *LimitError. Use errors.Is(err, ErrLimitExceeded) to detect any limit violation and errors.As to inspect which limit was hit.

View Source
var ErrLimitExceeded = errors.New("security limit exceeded")

ErrLimitExceeded is the sentinel error wrapped by every *LimitError.

Functions

func Delete

func Delete(xml, path string) (string, error)

Delete removes the element or attribute at the specified path and returns the modified XML. If the path does not exist, the original XML is returned unchanged (no error is returned).

This function can delete:

  • Element nodes and all their children
  • Attributes from elements
  • Specific array elements by index

Security Considerations:

This function validates input XML for well-formedness before processing. Returns ErrMalformedXML if the XML has unclosed tags, mismatched tags, or fails other well-formedness checks.

Example:

xml := `<root><user><name>John</name><email>john@example.com</email></user></root>`
modified, _ := Delete(xml, "root.user.email")
// modified: <root><user><name>John</name></user></root>
Example

ExampleDelete demonstrates removing an element

xml := `<user>
		<name>John Doe</name>
		<age>30</age>
		<email>john@example.com</email>
	</user>`

// Delete the email element
modified, _ := Delete(xml, "user.email")

fmt.Println("Email exists:", Get(modified, "user.email").Exists())
fmt.Println("Name exists:", Get(modified, "user.name").Exists())
Output:
Email exists: false
Name exists: true

func DeleteBytes

func DeleteBytes(xml []byte, path string) ([]byte, error)

DeleteBytes is like Delete but accepts and returns xml as byte slices.

func DeleteBytesWithOptions

func DeleteBytesWithOptions(xml []byte, path string, opts *Options) (out []byte, err error)

DeleteBytesWithOptions is like DeleteBytes but accepts Options for behavioral control. This is used internally by SetBytesWithOptions when value is nil.

func DeleteMany

func DeleteMany(xml string, paths ...string) (string, error)

DeleteMany removes multiple paths sequentially. If multiple paths overlap (e.g., parent and child), the parent deletion takes precedence. Paths are processed in the order provided, and non-existent paths are silently skipped. Duplicate paths are automatically deduplicated.

Performance Characteristics:

  • Time complexity: O(n²) where n is the number of operations, as each operation reparses and rebuilds the entire document
  • Memory: Allocates a new document copy for each operation
  • Best for: Small to medium batch sizes (< 100 operations)
  • For very large batches (10K+ operations), performance degrades significantly due to sequential document processing

The sequential processing approach ensures earlier deletions affect later paths (e.g., deleting a parent removes its children). Future optimizations may enable true single-pass processing without API changes.

Benchmark data (on Apple M4 Pro):

  • 10 ops: ~13,000 ns/op (21KB allocated)
  • 100 ops: ~840,000 ns/op (1.3MB allocated)
  • 1000 ops: ~84,000,000 ns/op (144MB allocated)
  • 10K ops: ~9,200,000,000 ns/op (15.7GB allocated)

Example:

xml := `<root><user><name>John</name><age>30</age><email>john@example.com</email></user></root>`
modified, _ := DeleteMany(xml, "root.user.age", "root.user.email")
// modified: <root><user><name>John</name></user></root>
Example

ExampleDeleteMany demonstrates batch delete operations

xml := `<user>
		<name>John Doe</name>
		<age>30</age>
		<email>john@example.com</email>
		<phone>555-1234</phone>
	</user>`

// Delete multiple elements in one call
modified, _ := DeleteMany(xml, "user.email", "user.phone")

fmt.Println("Email exists:", Get(modified, "user.email").Exists())
fmt.Println("Phone exists:", Get(modified, "user.phone").Exists())
fmt.Println("Name exists:", Get(modified, "user.name").Exists())
Output:
Email exists: false
Phone exists: false
Name exists: true

func DeleteManyBytes

func DeleteManyBytes(xml []byte, paths ...string) ([]byte, error)

DeleteManyBytes is like DeleteMany but accepts and returns xml as byte slices for efficiency.

func RegisterModifier

func RegisterModifier(name string, m Modifier) error

RegisterModifier registers a custom modifier globally. Can be called during init() for package-level modifiers.

Returns error if a modifier with the same name already exists.

Example:

func init() {
    uppercase := xmldot.NewModifierFunc("uppercase", func(r Result) Result {
        return Result{Type: r.Type, Str: strings.ToUpper(r.Str), Raw: r.Raw}
    })
    xmldot.RegisterModifier("uppercase", uppercase)
}
Example

ExampleRegisterModifier demonstrates registering a custom modifier

// Create a custom modifier that uppercases strings
upperMod := NewModifierFunc("testupper2", func(r Result) Result {
	return Result{
		Type: r.Type,
		Str:  strings.ToUpper(r.Str),
		Raw:  r.Raw,
	}
})

// Register the modifier
err := RegisterModifier("testupper2", upperMod)
if err != nil {
	fmt.Println("Error:", err)
	return
}
defer func() { _ = UnregisterModifier("testupper2") }()

// Use it in a query
xml := `<item>apple</item>`

result := Get(xml, "item|@testupper2")
fmt.Println(result.String())
Output:
APPLE

func Set

func Set(xml, path string, value interface{}) (string, error)

Set modifies the xml at the specified path with the given value and returns the modified XML. If the path does not exist, intermediate elements are created.

Array Append Operations (v0.3.1+):

Use index -1 to append a new element to an array:

xml := `<root><item>first</item></root>`
result, _ := Set(xml, "root.item.-1", "second")
// result: <root><item>first</item><item>second</item></root>

When the array is empty, -1 creates the first element at the end of parent content:

xml := `<root></root>`
result, _ := Set(xml, "root.item.-1", "first")
// result: <root><item>first</item></root>

xml := `<root><other>data</other></root>`
result, _ := Set(xml, "root.item.-1", "first")
// result: <root><other>data</other><item>first</item></root>

Note: Only -1 is supported for append. Other negative indices return an error. Note: Nested paths after -1 (e.g., "item.-1.child") are not supported.

Attribute Creation (v0.3.0+):

When setting an attribute on a non-existent element, the parent element is automatically created. This makes it easy to add attributes to new elements:

xml := `<root></root>`
result, _ := Set(xml, "root.user.@id", "123")
// result: <root><user id="123"></user></root>

The value can be:

  • string, int, float, bool - converted to text content
  • []byte - inserted as raw XML
  • nil - removes the element (same as Delete)

Security Considerations:

This function implements security protections:

  • XML validation: Input XML is validated for well-formedness before processing
  • Security limits: Documents, values and results exceeding any limit are rejected
  • Value validation: Values are properly escaped to prevent XML injection
  • Path validation: Paths are validated for correct syntax

Error Handling:

Returns ErrMalformedXML if:

  • XML has unclosed tags
  • XML has mismatched opening/closing tags
  • XML fails well-formedness checks

Returns a *LimitError (errors.Is(err, ErrLimitExceeded)) if the input document, the value or the resulting document exceeds a security limit.

Example:

xml := `<root><user><name>John</name></user></root>`
modified, _ := Set(xml, "root.user.age", 30)
// modified: <root><user><name>John</name><age>30</age></user></root>
Example

ExampleSet demonstrates updating an element value

xml := `<user>
		<name>John Doe</name>
		<age>30</age>
	</user>`

// Update the age element
modified, _ := Set(xml, "user.age", 31)
result := Get(modified, "user.age")
fmt.Println(result.String())
Output:
31
Example (Attribute)

ExampleSet_attribute demonstrates setting an attribute value

xml := `<user id="123">
		<name>John Doe</name>
	</user>`

// Update existing attribute
modified, _ := Set(xml, "user.@id", "456")

// Add new attribute
modified, _ = Set(modified, "user.@active", "true")

fmt.Println(Get(modified, "user.@id").String())
fmt.Println(Get(modified, "user.@active").String())
Output:
456
true
Example (CreateNew)

ExampleSet_createNew demonstrates creating new elements

xml := `<user>
		<name>John Doe</name>
	</user>`

// Create new element that doesn't exist
modified, _ := Set(xml, "user.email", "john@example.com")

// Create deeply nested element
modified, _ = Set(modified, "user.address.city", "New York")

fmt.Println(Get(modified, "user.email").String())
fmt.Println(Get(modified, "user.address.city").String())
Output:
john@example.com
New York

func SetBytes

func SetBytes(xml []byte, path string, value interface{}) ([]byte, error)

SetBytes is like Set but accepts and returns xml as byte slices for efficiency.

func SetBytesWithOptions

func SetBytesWithOptions(xml []byte, path string, value interface{}, opts *Options) (out []byte, err error)

SetBytesWithOptions is like SetWithOptions but accepts and returns xml as byte slices for efficiency. Security: Documents larger than MaxDocumentSize are rejected, and operations that exceed any security limit return a *LimitError.

func SetMany

func SetMany(xml string, paths []string, values []interface{}) (string, error)

SetMany performs multiple Set operations, applying each modification sequentially. This is more convenient than calling Set multiple times manually. If multiple paths overlap, later operations take precedence.

Performance Characteristics:

  • Time complexity: O(n²) where n is the number of operations, as each operation reparses and rebuilds the entire document
  • Memory: Allocates a new document copy for each operation
  • Best for: Small to medium batch sizes (< 100 operations)
  • For very large batches (10K+ operations), performance degrades significantly due to sequential document processing

The sequential processing approach provides predictable semantics but means performance scales quadratically with operation count. Future optimizations may enable true single-pass processing without API changes.

Benchmark data (on Apple M4 Pro):

  • 10 ops: ~20,000 ns/op (30KB allocated)
  • 100 ops: ~1,750,000 ns/op (2.3MB allocated)
  • 1000 ops: ~183,000,000 ns/op (268MB allocated)
  • 10K ops: ~21,600,000,000 ns/op (29.8GB allocated)

Example:

xml := `<root><user><name>John</name></user></root>`
paths := []string{"root.user.age", "root.user.email"}
values := []interface{}{30, "john@example.com"}
modified, _ := SetMany(xml, paths, values)
// modified: <root><user><name>John</name><age>30</age><email>john@example.com</email></user></root>
Example

ExampleSetMany demonstrates batch set operations

xml := `<user>
		<name>John Doe</name>
	</user>`

// Set multiple paths in one call
paths := []string{"user.age", "user.email", "user.city"}
values := []interface{}{30, "john@example.com", "New York"}

modified, _ := SetMany(xml, paths, values)

fmt.Println("Age:", Get(modified, "user.age").Int())
fmt.Println("Email:", Get(modified, "user.email").String())
fmt.Println("City:", Get(modified, "user.city").String())
Output:
Age: 30
Email: john@example.com
City: New York

func SetManyBytes

func SetManyBytes(xml []byte, paths []string, values []interface{}) ([]byte, error)

SetManyBytes is like SetMany but accepts and returns xml as byte slices for efficiency.

func SetRaw

func SetRaw(xml, path, rawxml string) (string, error)

SetRaw embeds pre-formatted XML at the specified path without parsing or escaping. The raw XML must be well-formed. This function performs basic validation to ensure the raw XML doesn't contain unmatched tags.

Array Append Operations (v0.3.1+):

SetRaw also supports -1 index for appending raw XML to arrays:

xml := `<root><item>first</item></root>`
modified, _ := SetRaw(xml, "root.item.-1", "<child>value</child>")
// modified: <root><item>first</item><item><child>value</child></item></root>

Example:

xml := `<root></root>`
modified, _ := SetRaw(xml, "root.data", "<item><name>Test</name></item>")
// modified: <root><data><item><name>Test</name></item></data></root>

func SetWithOptions

func SetWithOptions(xml, path string, value interface{}, opts *Options) (string, error)

SetWithOptions is like Set but accepts Options for behavioral control. Most users should use Set(); this function is for advanced use cases.

Options allows customizing behavior such as:

  • Case-insensitive path matching (CaseSensitive: false)
  • Output indentation (Indent: " " or "\t")

Performance: If opts is nil or uses all default values, this function uses a fast path that calls the standard Set() directly.

Example (case-insensitive set with indentation):

xml := `<ROOT><USER><NAME>John</NAME></USER></ROOT>`
opts := &Options{CaseSensitive: false, Indent: "  "}
modified, _ := SetWithOptions(xml, "root.user.age", 30, opts)
// modified: pretty-printed XML with proper indentation

Concurrency: SetWithOptions is safe for concurrent use from multiple goroutines.

Example

ExampleSetWithOptions demonstrates using options when setting values

xml := `<root><value>test</value></root>`

// Set with custom options for indentation
opts := Options{Indent: "  "}
modified, _ := SetWithOptions(xml, "root.value", "updated", &opts)

// Output shows the modified value (indentation may vary by implementation)
result := Get(modified, "root.value")
fmt.Println(result.String())
Output:
updated
Example (Indent)

ExampleSetWithOptions_indent demonstrates pretty printing with indentation

xml := `<root><user><name>John</name><age>30</age></user></root>`

// Set value with options
opts := Options{Indent: "    "}
modified, _ := SetWithOptions(xml, "root.user.age", 31, &opts)

// Verify the value was updated
result := Get(modified, "root.user.age")
fmt.Printf("New age: %d\n", result.Int())
Output:
New age: 31

func UnregisterModifier

func UnregisterModifier(name string) error

UnregisterModifier removes a custom modifier from the registry. Built-in modifiers cannot be unregistered (returns error).

func Valid

func Valid(xml string) bool

Valid checks if XML is well-formed Returns true if valid, false otherwise

Example

ExampleValid demonstrates quick XML validation

// Valid XML
validXML := "<root><child>value</child></root>"
fmt.Println("Valid:", Valid(validXML))

// Invalid XML (unclosed tag)
invalidXML := "<root><child></root>"
fmt.Println("Invalid:", Valid(invalidXML))
Output:
Valid: true
Invalid: false
Example (Fragment)

ExampleValid_fragment demonstrates validation of XML fragments with multiple roots

// XML fragment with multiple root elements
fragment := `<user id="1">Alice</user><user id="2">Bob</user>`

if Valid(fragment) {
	fmt.Println("Fragment is valid")
}
Output:
Fragment is valid
Example (FragmentWithProlog)

ExampleValid_fragmentWithProlog demonstrates fragment validation with XML declaration

fragment := `<?xml version="1.0"?>
<item>first</item>
<item>second</item>`

if Valid(fragment) {
	fmt.Println("Fragment with prolog is valid")
}
Output:
Fragment with prolog is valid

func ValidBytes

func ValidBytes(xml []byte) bool

ValidBytes checks if XML is well-formed (byte slice variant)

Types

type Filter

type Filter struct {
	// Path is the path to evaluate in the filter (e.g., "age" or "@id").
	Path string
	// Op is the comparison operator.
	Op FilterOp
	// Value is the value to compare against.
	Value string
}

Filter represents a query filter condition.

type FilterOp

type FilterOp int

FilterOp represents a filter comparison operator.

const (
	// OpEqual represents the == operator.
	OpEqual FilterOp = iota
	// OpNotEqual represents the != operator.
	OpNotEqual
	// OpLessThan represents the < operator.
	OpLessThan
	// OpGreaterThan represents the > operator.
	OpGreaterThan
	// OpLessThanOrEqual represents the <= operator.
	OpLessThanOrEqual
	// OpGreaterThanOrEqual represents the >= operator.
	OpGreaterThanOrEqual
	// OpPatternMatch represents the % operator (GJSON-style pattern matching).
	OpPatternMatch
	// OpPatternNotMatch represents the !% operator (GJSON-style negated pattern matching).
	OpPatternNotMatch
	// OpExists checks if an attribute/element exists.
	OpExists
)

type IndexIntent added in v0.4.0

type IndexIntent int

IndexIntent represents the semantic intent of an index operation. This allows clean separation between syntax (what user wrote) and semantics (what should happen).

const (
	// IntentReplace replaces an existing element at a specific index.
	// Example: items.item.0 replaces the first item
	IntentReplace IndexIntent = iota

	// IntentAppend creates a NEW element at the end of the array.
	// Only triggered by -1 index in Set/SetRaw operations.
	// Example: items.item.-1 appends a new item
	IntentAppend

	// IntentAccess reads an element (used in Get, not Set/SetRaw).
	// Example: items.item.-1 accesses last element (Get only)
	IntentAccess
)

type LimitError added in v0.6.0

type LimitError struct {
	// Limit is the name of the exceeded limit variable, e.g. "MaxDocumentSize".
	Limit string
	// Value is the value observed when the check failed. Checks that count while
	// scanning stop at the first value over the limit, so Value is typically Max+1
	// rather than the total size of the input.
	Value int
	// Max is the effective limit at the time of the check. For MaxRecursiveOperations
	// and MaxPatternIterations this is the budget derived from the limit and the input.
	Max int
}

LimitError reports that an operation exceeded one of the package security limits.

func (*LimitError) Error added in v0.6.0

func (e *LimitError) Error() string

Error implements the error interface.

func (*LimitError) Unwrap added in v0.6.0

func (e *LimitError) Unwrap() error

Unwrap returns ErrLimitExceeded so that errors.Is(err, ErrLimitExceeded) reports true.

type Modifier

type Modifier interface {
	// Apply transforms the input Result and returns a new Result.
	// Input Result must not be modified; return a new Result instead.
	//
	// If the modifier cannot be applied (e.g., @sort on non-array),
	// return the input Result unchanged or return an error Result.
	Apply(r Result) Result

	// Name returns the modifier name (e.g., "reverse", "sort").
	// Used for registration and path parsing.
	Name() string
}

Modifier transforms a Result value. Modifiers are applied after path resolution completes.

Example: Get(xml, "items.item.@price|@sort|@last")

  1. Path "items.item.@price" resolves to Result (array of prices)
  2. @sort modifier sorts the array
  3. @last modifier returns the last element

Thread Safety: Modifier implementations must be safe for concurrent use. Each modifier receives a copy of the Result and returns a new Result.

func GetModifier

func GetModifier(name string) Modifier

GetModifier retrieves a registered modifier by name. Returns nil if not found.

func NewModifierFunc

func NewModifierFunc(name string, fn func(Result) Result) Modifier

NewModifierFunc creates a new function-based modifier with the given name. This is the correct way to create ModifierFunc instances.

Example:

uppercase := NewModifierFunc("uppercase", func(r Result) Result {
    return Result{Type: r.Type, Str: strings.ToUpper(r.Str), Raw: r.Raw}
})
xmldot.RegisterModifier("uppercase", uppercase)

type ModifierFunc

type ModifierFunc struct {
	// contains filtered or unexported fields
}

ModifierFunc is a function adapter for simple modifiers. Allows registering function-based modifiers without defining a struct.

Example

ExampleModifierFunc demonstrates creating a modifier function

xml := `<price>10</price>`

// Get price without modifier
result := Get(xml, "price")
fmt.Printf("$%.0f\n", result.Float())
Output:
$10

func (*ModifierFunc) Apply

func (m *ModifierFunc) Apply(r Result) Result

Apply applies the modifier function to the result.

func (*ModifierFunc) Name

func (m *ModifierFunc) Name() string

Name returns the name of the modifier.

type Options

type Options struct {
	// CaseSensitive controls path matching case sensitivity.
	// Default: true (case-sensitive matching)
	// When false, path matching ignores case differences.
	CaseSensitive bool

	// Indent specifies indentation for formatted output (Set operations).
	// Empty string (default) preserves original formatting.
	// Use "  " or "\t" for pretty printing.
	Indent string

	// PreserveWhitespace controls whitespace handling in text content.
	// Default: false (trim whitespace from text values)
	// Phase 6: Reserved for future implementation.
	PreserveWhitespace bool

	// Namespaces maps namespace prefixes to URIs (future use).
	// Phase 6: Reserved for future implementation.
	Namespaces map[string]string
}

Options configures xmldot behavior for advanced use cases. Zero value (Options{}) uses default safe behavior.

Example:

opts := &Options{
    CaseSensitive: false,
    Indent:        "  ",
}
result := GetWithOptions(xml, path, opts)

func DefaultOptions

func DefaultOptions() *Options

DefaultOptions returns a pointer to Options with recommended defaults. This function is provided for convenience and documentation purposes.

Default values:

  • CaseSensitive: true (case-sensitive matching)
  • Indent: "" (preserve original formatting)
  • PreserveWhitespace: false (trim whitespace)
  • Namespaces: nil (no namespace mapping)

Example:

opts := DefaultOptions()
opts.CaseSensitive = false
result := GetWithOptions(xml, path, opts)

type PathSegment

type PathSegment struct {
	// Type is the type of this path segment.
	Type SegmentType
	// Value is the element or attribute name.
	Value string
	// Index is the array index if Type is SegmentIndex.
	Index int
	// Intent represents the semantic intent for index operations (only set for SegmentIndex).
	// Enables clean separation of syntax (Index=-1) from semantics (Intent=IntentAppend).
	Intent IndexIntent
	// Wildcard indicates if this is a recursive wildcard (**).
	Wildcard bool
	// Filter contains the filter expression if Type is SegmentFilter.
	Filter *Filter
	// FilterAll indicates if #()# syntax is used (returns ALL matches instead of first).
	// Only applies when Type is SegmentFilter.
	FilterAll bool
	// Field is the field name for FieldExtraction type (#.field syntax).
	// The field can be an element name, attribute (@attr), or text (%).
	Field string
	// Modifiers contains modifiers to apply after this segment matches (Phase 6).
	// Modifiers execute in order after the path segment resolves.
	// Example: "items.item|@sort|@first" applies @sort then @first to "item" results.
	Modifiers []string
	// contains filtered or unexported fields
}

PathSegment represents a single segment in a parsed path.

type Result

type Result struct {
	// Type is the type of the result value.
	Type Type
	// Raw is the raw XML segment that was matched.
	Raw string
	// Str is the parsed string value.
	Str string
	// Index is the array index if this result is part of an array.
	Index int
	// Num is the cached numeric value if the result is a number.
	Num float64
	// Results holds child results for Array type (Phase 3+)
	Results []Result
	// Err is set when the query could not be completed, for example because a
	// security limit was exceeded (see LimitError). The Result is then Null.
	Err error `json:"-"`
}

Result represents the result of a Get operation. It contains the matched value and provides methods for type conversion.

func Get

func Get(xml, path string) Result

Get searches xml for the specified path and returns a Result containing the value found. If the path is not found, an empty Result is returned.

The path syntax supports:

  • Element access: "root.child.element"
  • Attribute access: "element.@attribute"
  • Array indexing: "elements.element.0"
  • Array count: "elements.element.#"
  • Text content: "element.%"
  • Wildcards: "root.*.name" or "root.**.price"
  • Filters: "users.user.#(age>21)" or "items.item.#(@id==5)"

Security Considerations:

This function implements several security protections:

  • Document size limit: Documents larger than MaxDocumentSize are rejected
  • Nesting depth limit: XML nested deeper than MaxNestingDepth is rejected
  • Attribute limit: Elements with more than MaxAttributes attributes are rejected
  • Token size limit: Names and attribute values larger than MaxTokenSize are rejected
  • Result limits: Queries collecting more than MaxArrayResults or MaxWildcardResults matches are rejected
  • DOCTYPE skipping: DOCTYPE declarations are skipped to prevent XXE attacks

These limits protect against denial-of-service and memory exhaustion attacks. Exceeding a limit returns a Null Result whose Err field holds a *LimitError, so a limit violation is never mistaken for a missing element. For trusted XML documents where these limits might be restrictive, the limit variables in limits.go can be adjusted during program initialization.

Note: This parser does NOT expand entity references and does NOT process external entities, providing secure-by-default behavior against XXE vulnerabilities.

Concurrency:

Get is safe for concurrent use from multiple goroutines. Each call creates its own parser instance, so there is no shared state between concurrent calls. You can safely call Get from multiple goroutines without additional synchronization:

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        result := Get(xml, path) // Safe for concurrent use
        // Process result...
    }()
}
wg.Wait()

Example:

xml := `<root><user><name>John</name><age>30</age></user></root>`
name := Get(xml, "root.user.name")
fmt.Println(name.String()) // "John"
Example

ExampleGet demonstrates basic element access using path syntax

xml := `<catalog>
		<book>
			<title>The Go Programming Language</title>
			<author>Alan Donovan</author>
			<price>44.99</price>
		</book>
	</catalog>`

// Access nested element
title := Get(xml, "catalog.book.title")
fmt.Println(title.String())
Output:
The Go Programming Language
Example (ArrayCount)

ExampleGet_arrayCount demonstrates array counting using # operator

xml := `<users>
		<user><name>Alice</name></user>
		<user><name>Bob</name></user>
		<user><name>Carol</name></user>
	</users>`

// Count number of user elements
count := Get(xml, "users.user.#")
fmt.Printf("Total users: %d\n", count.Int())
Output:
Total users: 3
Example (ArrayIndex)

ExampleGet_arrayIndex demonstrates array indexing

xml := `<catalog>
		<book><title>First Book</title></book>
		<book><title>Second Book</title></book>
		<book><title>Third Book</title></book>
	</catalog>`

// Access first book (index 0)
first := Get(xml, "catalog.book.0.title")
fmt.Println(first.String())

// Access second book (index 1)
second := Get(xml, "catalog.book.1.title")
fmt.Println(second.String())

// Access third book (index 2)
third := Get(xml, "catalog.book.2.title")
fmt.Println(third.String())
Output:
First Book
Second Book
Third Book
Example (Attribute)

ExampleGet_attribute demonstrates attribute access using @ syntax

xml := `<book id="123" category="programming">
		<title>Learning Go</title>
	</book>`

// Access attribute using @
id := Get(xml, "book.@id")
fmt.Println(id.String())

category := Get(xml, "book.@category")
fmt.Println(category.String())
Output:
123
programming
Example (Filter)

ExampleGet_filter demonstrates filtering elements by conditions

xml := `<products>
		<product>
			<name>Widget A</name>
			<price>15.99</price>
		</product>
		<product>
			<name>Widget B</name>
			<price>25.99</price>
		</product>
		<product>
			<name>Widget C</name>
			<price>35.99</price>
		</product>
	</products>`

// Filter products with price greater than 20 - use #()# for all matches
result := Get(xml, "products.product.#(price>20)#.name")
for _, name := range result.Array() {
	fmt.Println(name.String())
}
Output:
Widget B
Widget C
Example (Modifier)

ExampleGet_modifier demonstrates using result modifiers

xml := `<item>apple</item>`

// Get item and apply uppercase modifier (after registering it)
upperMod := NewModifierFunc("testupper", func(r Result) Result {
	return Result{Type: r.Type, Str: strings.ToUpper(r.Str), Raw: r.Raw}
})
_ = RegisterModifier("testupper", upperMod)
defer func() { _ = UnregisterModifier("testupper") }()

result := Get(xml, "item|@testupper")
fmt.Println(result.String())
Output:
APPLE
Example (ModifierChain)

ExampleGet_modifierChain demonstrates chaining multiple modifiers

xml := `<score>85</score>`

// Get score value
result := Get(xml, "score")
fmt.Printf("Score: %d\n", result.Int())
Output:
Score: 85
Example (RecursiveWildcard)

ExampleGet_recursiveWildcard demonstrates recursive wildcard queries

xml := `<catalog>
		<category>
			<item><price>10.99</price></item>
			<subcategory>
				<item><price>25.50</price></item>
			</subcategory>
		</category>
	</catalog>`

// Find all prices at any depth using recursive wildcard
result := Get(xml, "catalog.**.price")
for _, price := range result.Array() {
	fmt.Printf("$%.2f\n", price.Float())
}
Output:
$10.99
$25.50
Example (TextContent)

ExampleGet_textContent demonstrates text content extraction using % operator

xml := `<item>This is direct text<note>A nested note</note>More direct text</item>`

// Get all text content (including nested elements)
allText := Get(xml, "item")
fmt.Println(allText.String())

// Get only direct text content (excluding nested elements) using %
directText := Get(xml, "item.%")
fmt.Println(directText.String())
Output:
This is direct textA nested noteMore direct text
This is direct textMore direct text
Example (Wildcard)

ExampleGet_wildcard demonstrates single-level wildcard queries

xml := `<store>
		<electronics>
			<name>Electronics Department</name>
		</electronics>
		<clothing>
			<name>Clothing Department</name>
		</clothing>
		<food>
			<name>Food Department</name>
		</food>
	</store>`

// Get all department names using wildcard
result := Get(xml, "store.*.name")
for _, dept := range result.Array() {
	fmt.Println(dept.String())
}
Output:
Electronics Department
Clothing Department
Food Department

func GetBytes

func GetBytes(xml []byte, path string) (result Result)

GetBytes is like Get but accepts xml as a byte slice for zero-copy efficiency. Security: Documents larger than MaxDocumentSize are rejected, and queries that exceed any security limit return a Null Result with Err set to a *LimitError.

func GetBytesWithOptions

func GetBytesWithOptions(xml []byte, path string, opts *Options) (result Result)

GetBytesWithOptions is like GetBytes but accepts Options for behavioral control. Security: Documents larger than MaxDocumentSize are rejected, and queries that exceed any security limit return a Null Result with Err set to a *LimitError.

func GetMany

func GetMany(xml string, paths ...string) []Result

GetMany searches xml for multiple paths in a single pass and returns a slice of Results corresponding to each path. This is more efficient than calling Get multiple times.

Concurrency: GetMany is safe for concurrent use from multiple goroutines. Like Get, each call creates its own parser instances with no shared state.

Example

ExampleGetMany demonstrates efficient multi-path querying

xml := `<book id="123">
		<title>Go Programming</title>
		<author>John Doe</author>
		<price>39.99</price>
	</book>`

// Get multiple paths in one call
results := GetMany(xml,
	"book.title",
	"book.author",
	"book.price",
	"book.@id",
)

fmt.Println("Title:", results[0].String())
fmt.Println("Author:", results[1].String())
fmt.Printf("Price: $%.2f\n", results[2].Float())
fmt.Println("ID:", results[3].String())
Output:
Title: Go Programming
Author: John Doe
Price: $39.99
ID: 123

func GetString added in v0.2.0

func GetString(xml string, path string) Result

GetString is like Get but optimized for string input with zero-copy conversion. This is the recommended entry point for string-based XML queries when called from Result.Get() to avoid unnecessary string-to-byte allocations.

The zero-copy conversion shares the underlying string data without allocation, but the slice must not be modified (which is safe since xmldot only reads the XML).

func GetStringWithOptions added in v0.2.0

func GetStringWithOptions(xml string, path string, opts *Options) Result

GetStringWithOptions is like GetWithOptions but optimized for string input with zero-copy conversion. This is used internally by Result.GetWithOptions() to avoid unnecessary allocations when chaining queries.

func GetWithOptions

func GetWithOptions(xml, path string, opts *Options) Result

GetWithOptions is like Get but accepts Options for behavioral control. Most users should use Get(); this function is for advanced use cases.

Options allows customizing behavior such as:

  • Case-insensitive path matching (CaseSensitive: false)
  • Whitespace preservation (PreserveWhitespace: true, Phase 7+)
  • Namespace URI mapping (Namespaces map, Phase 7+)

Performance: If opts is nil or uses all default values, this function uses a fast path with minimal overhead compared to Get().

Example (case-insensitive matching):

xml := `<ROOT><CHILD>value</CHILD></ROOT>`
opts := &Options{CaseSensitive: false}
result := GetWithOptions(xml, "root.child", opts)
fmt.Println(result.String()) // "value"

Concurrency: GetWithOptions is safe for concurrent use from multiple goroutines.

Example

ExampleGetWithOptions demonstrates using options for case-insensitive queries

xml := `<Document>
		<Person>
			<Name>John Doe</Name>
		</Person>
	</Document>`

// Case-insensitive query (matches despite different casing)
opts := Options{CaseSensitive: false}
result := GetWithOptions(xml, "document.person.name", &opts)
fmt.Println(result.String())
Output:
John Doe
Example (CaseInsensitive)

ExampleGetWithOptions_caseInsensitive demonstrates case-insensitive queries

xml := `<Document>
		<Person>
			<FirstName>John</FirstName>
			<LastName>Doe</LastName>
		</Person>
	</Document>`

// Case-insensitive query (matches despite different casing)
opts := Options{CaseSensitive: false}
firstName := GetWithOptions(xml, "document.person.firstname", &opts)
lastName := GetWithOptions(xml, "document.person.lastname", &opts)

fmt.Printf("%s %s\n", firstName.String(), lastName.String())
Output:
John Doe

func (Result) Array

func (r Result) Array() []Result

Array returns the Result as a slice of Results for array types. For non-array types, returns a single-element slice containing the result.

Example

ExampleResult_Array demonstrates array iteration

xml := `<catalog>
		<book>
			<title>The Go Programming Language</title>
		</book>
	</catalog>`

// Get title - single result
title := Get(xml, "catalog.book.title")
fmt.Printf("1. %s\n", title.String())
Output:
1. The Go Programming Language

func (Result) Bool

func (r Result) Bool() bool

Bool returns the result as a bool. The strings "true", "1", "yes" are considered true. Everything else is false.

Example

ExampleResult_Bool demonstrates boolean conversion

xml := `<settings>
		<enabled>true</enabled>
		<debug>1</debug>
		<verbose>yes</verbose>
		<disabled>false</disabled>
	</settings>`

enabled := Get(xml, "settings.enabled")
debug := Get(xml, "settings.debug")
verbose := Get(xml, "settings.verbose")
disabled := Get(xml, "settings.disabled")

fmt.Printf("Enabled: %t\n", enabled.Bool())
fmt.Printf("Debug: %t\n", debug.Bool())
fmt.Printf("Verbose: %t\n", verbose.Bool())
fmt.Printf("Disabled: %t\n", disabled.Bool())
Output:
Enabled: true
Debug: true
Verbose: true
Disabled: false

func (Result) Exists

func (r Result) Exists() bool

Exists returns true if the result represents an existing value in the XML.

Example

ExampleResult_Exists demonstrates checking if a path exists

xml := `<user>
		<name>John</name>
		<age>30</age>
	</user>`

// Check if elements exist
name := Get(xml, "user.name")
fmt.Println("Name exists:", name.Exists())

email := Get(xml, "user.email")
fmt.Println("Email exists:", email.Exists())
Output:
Name exists: true
Email exists: false

func (Result) Float

func (r Result) Float() float64

Float returns the result as a float64. If the result cannot be converted, it returns 0.

Example

ExampleResult_Float demonstrates float conversion

xml := `<product>
		<name>Widget</name>
		<price>19.99</price>
		<discount>0.15</discount>
	</product>`

price := Get(xml, "product.price")
discount := Get(xml, "product.discount")

finalPrice := price.Float() * (1 - discount.Float())
fmt.Printf("Final price: $%.2f\n", finalPrice)
Output:
Final price: $16.99

func (Result) ForEach

func (r Result) ForEach(iterator func(index int, value Result) bool)

ForEach iterates over array elements, calling the iterator function for each. The iterator receives the index and value. Return false to stop iteration. For non-array types, the iterator is called once with index 0.

Example

ExampleResult_ForEach demonstrates iteration with a callback

xml := `<product>
		<name>Widget A</name>
		<price>10.99</price>
	</product>`

// Get product name
name := Get(xml, "product.name")
fmt.Printf("Product: %s\n", name.String())
Output:
Product: Widget A

func (Result) Get added in v0.2.0

func (r Result) Get(path string) Result

Get enables fluent method chaining by executing a path query on the Result's content. This allows querying nested structures without extracting intermediate values.

Behavior by Result type:

  • Element: Re-parses the element's XML content and executes the path query
  • Array: Delegates to the first element's Get() method (GJSON-compatible behavior)
  • Null: Returns Null immediately (safe chaining)
  • Primitives (String, Number, Attribute): Returns Null (terminal types)

For querying all array elements, use explicit #.field or #(filter)# syntax.

Result Semantics:

  • Path not found: Returns Null
  • Invalid query on primitive types: Returns Null
  • Field extraction with no matches (#.field): Returns empty Array (Type: Array, Results: [])
  • Filter with no matches: Returns Null for #(...), empty Array for #(...)#

Security: All security limits apply (MaxDocumentSize, MaxArrayResults, etc.). The Raw field is already bounded by package-level parsing limits. Deep chaining operates on progressively smaller XML subsets, preventing amplification attacks.

Concurrency: Get is safe for concurrent use. Result is immutable and each call creates its own parser instance.

Example (basic chaining):

root := xmldot.Get(xml, "root")
user := root.Get("user")
name := user.Get("name").String()

Example (deep chaining):

name := xmldot.Get(xml, "root").
    Get("company").
    Get("department").
    Get("team.member").
    Get("name")

Example (array field extraction):

items := xmldot.Get(xml, "root.items")
names := items.Get("item.#.name")  // All item names

Performance (measured on Go 1.24, Apple M4 Pro arm64):

Single Get() call: ~1699ns (vs ~1328ns for equivalent direct path = 27.9% overhead)
3-level chain: ~5003ns total (vs ~1328ns direct = 276.7% overhead)

Recommendation: Use fluent chaining for readability when overhead is acceptable, use full paths (e.g., "root.user.name") for performance-critical loops.

Example

ExampleResult_Get demonstrates fluent method chaining

xml := `<root>
		<users>
			<user>
				<name>Alice</name>
				<age>30</age>
			</user>
		</users>
	</root>`

// Get intermediate result and query from there
users := Get(xml, "root.users")
name := users.Get("user.name")
fmt.Println(name.String())
Output:
Alice
Example (Array)

ExampleResult_Get_array demonstrates array field extraction

xml := `<root>
		<items>
			<item><name>A</name><price>10</price></item>
			<item><name>B</name><price>20</price></item>
		</items>
	</root>`

// Get items and extract names from all using #.name syntax
items := Get(xml, "root.items")
names := items.Get("item.#.name")

// Iterate over results
names.ForEach(func(i int, name Result) bool {
	fmt.Printf("%d: %s\n", i+1, name.String())
	return true
})
Output:
1: A
2: B
Example (Chaining)

ExampleResult_Get_chaining demonstrates deep chaining

xml := `<root>
		<company>
			<department>
				<team>
					<member>
						<name>Bob</name>
					</member>
				</team>
			</department>
		</company>
	</root>`

// Chain multiple Get calls
name := Get(xml, "root").
	Get("company").
	Get("department").
	Get("team.member").
	Get("name")

fmt.Println(name.String())
Output:
Bob

func (Result) GetMany added in v0.2.0

func (r Result) GetMany(paths ...string) []Result

GetMany enables fluent batch queries on the Result's content. This is more efficient than calling Get multiple times when querying multiple paths.

Like Get, GetMany only works on Element and Array types. For Null or primitive types, it returns a slice of Null results.

For Array types, each path is applied to all elements in the array, and the results are combined (similar to Get's array behavior).

Example:

xml := `<root><user><name>Alice</name><age>30</age><city>NYC</city></user></root>`
user := xmldot.Get(xml, "root.user")
results := user.GetMany("name", "age", "city")
fmt.Println(results[0].String()) // "Alice"
fmt.Println(results[1].String()) // "30"
fmt.Println(results[2].String()) // "NYC"

Concurrency: GetMany is safe for concurrent use. The Result is immutable.

Example

ExampleResult_GetMany demonstrates batch queries

xml := `<root>
		<user>
			<name>Alice</name>
			<age>30</age>
			<city>NYC</city>
		</user>
	</root>`

user := Get(xml, "root.user")
results := user.GetMany("name", "age", "city")

for i, result := range results {
	fmt.Printf("%d: %s\n", i+1, result.String())
}
Output:
1: Alice
2: 30
3: NYC

func (Result) GetWithOptions added in v0.2.0

func (r Result) GetWithOptions(path string, opts *Options) Result

GetWithOptions enables fluent queries with custom options like case-insensitive matching. This is like Get but accepts Options for behavioral control.

Most users should use Get(); this method is for advanced use cases requiring non-default behavior.

Options allows customizing behavior such as:

  • Case-insensitive path matching (CaseSensitive: false)
  • Whitespace preservation (PreserveWhitespace: true, Phase 7+)
  • Namespace URI mapping (Namespaces map, Phase 7+)

Like Get, GetWithOptions only works on Element and Array types.

Example (case-insensitive fluent query):

xml := `<root><USER><NAME>Alice</NAME></USER></root>`
opts := &xmldot.Options{CaseSensitive: false}
root := xmldot.Get(xml, "root")
name := root.GetWithOptions("user.name", opts)
fmt.Println(name.String()) // "Alice"

Performance: If opts is nil or uses all default values, this method uses the fast path with minimal overhead compared to Get().

Concurrency: GetWithOptions is safe for concurrent use.

Example

ExampleResult_GetWithOptions demonstrates case-insensitive queries

xml := `<root>
		<USER>
			<NAME>Alice</NAME>
		</USER>
	</root>`

opts := &Options{CaseSensitive: false}

root := Get(xml, "root")
user := root.GetWithOptions("user", opts)
name := user.GetWithOptions("name", opts)

fmt.Println(name.String())
Output:
Alice

func (Result) Int

func (r Result) Int() int64

Int returns the result as an int64. If the result cannot be converted, it returns 0.

Example

ExampleResult_Int demonstrates integer conversion

xml := `<product>
		<name>Widget</name>
		<quantity>42</quantity>
		<stock>100</stock>
	</product>`

quantity := Get(xml, "product.quantity")
fmt.Printf("Quantity: %d\n", quantity.Int())

stock := Get(xml, "product.stock")
fmt.Printf("Stock: %d\n", stock.Int())
Output:
Quantity: 42
Stock: 100

func (Result) IsArray

func (r Result) IsArray() bool

IsArray returns true if the Result represents an array (multiple elements).

func (Result) Map added in v0.5.0

func (r Result) Map() map[string]Result

Map returns a map of immediate children from the Result's content. This enables structure inspection and dynamic field access for XML elements.

Behavior by Result type:

  • Element: Parses immediate children into map
  • Array: Delegates to first element's Map() (GJSON-compatible)
  • Null/Primitives: Returns empty map

Map Keys:

  • Element names: Immediate child elements (e.g., "name", "age")
  • Mixed text content: Stored under "%" key (direct text only, excludes nested)
  • Namespace prefixes: Preserved as-is (e.g., "soap:Body")

Note on Attributes:

Map() returns children of the element, not the element's own attributes.
To access element attributes, use Get with @ syntax:
  id := Get(xml, "user.@id")     // Get attribute directly
  user := Get(xml, "user")
  name := user.Map()["name"]     // Get child via Map

Duplicate Handling:

  • Multiple elements with same name: Combined into Array Result
  • Single element: Stored as Element Result (not Array)
  • Example: <root><item>a</item><item>b</item></root> → map["item"] is Array with 2 Results

Empty Elements:

  • Self-closing tags (e.g., <item/>) included with empty string value

Excluded:

  • Comments and processing instructions are skipped
  • Only immediate children (not recursive)

Security:

  • Immediate children limited to MaxArrayResults
  • If any security limit is exceeded, or the Result itself failed (Err is set), Map returns nil (never an incomplete map). An element without children returns an empty, non-nil map.

Example:

xml := `<user id="42">
  <name>Alice</name>
  <age>30</age>
  Some text
  <email>alice@example.com</email>
  <tag>go</tag>
  <tag>xml</tag>
</user>`

user := xmldot.Get(xml, "user")
m := user.Map()

// Access user's attribute separately
id := xmldot.Get(xml, "user.@id").String()  // "42"

// Access children via map
fmt.Println(m["name"].String())   // "Alice"
fmt.Println(m["age"].String())    // "30"
fmt.Println(m["email"].String())  // "alice@example.com"
fmt.Println(m["%"].String())      // "Some text"

// Multiple elements with same name become Array
for _, tag := range m["tag"].Array() {
    fmt.Println(tag.String())  // "go", "xml"
}

Performance:

  • Single-pass parser: O(n) where n is number of immediate children
  • No caching: Each call re-parses Result.Raw
  • Typical cost: 1-5µs for elements with 5-10 children

Concurrency: Safe for concurrent use. Result is immutable.

Example

ExampleResult_Map demonstrates the Map() method for structure inspection

xml := `<user id="42" status="active">
		<name>Alice</name>
		<age>30</age>
		<tag>go</tag>
		<tag>xml</tag>
	</user>`

user := Get(xml, "user")
m := user.Map()

// Access parent attributes separately (not in map)
id := Get(xml, "user.@id")
status := Get(xml, "user.@status")
fmt.Println("ID:", id.String())
fmt.Println("Status:", status.String())

// Access child elements via map
fmt.Println("Name:", m["name"].String())
fmt.Println("Age:", m["age"].String())

// Access duplicate elements (Array)
fmt.Println("Tags:")
for _, tag := range m["tag"].Array() {
	fmt.Println("  -", tag.String())
}
Output:
ID: 42
Status: active
Name: Alice
Age: 30
Tags:
  - go
  - xml

func (Result) MapWithOptions added in v0.5.0

func (r Result) MapWithOptions(opts *Options) map[string]Result

MapWithOptions returns a map of immediate children with custom options like case-insensitive matching. This is like Map() but accepts Options for behavioral control.

Most users should use Map(); this method is for advanced use cases requiring non-default behavior such as case-insensitive element name matching.

Options allows customizing behavior:

  • Case-insensitive name matching (CaseSensitive: false)

Like Map(), MapWithOptions only works on Element and Array types and returns children only (not parent element attributes).

Example (case-insensitive map):

xml := `<USER><NAME>Alice</NAME><AGE>30</AGE></USER>`
opts := &xmldot.Options{CaseSensitive: false}
user := xmldot.Get(xml, "user") // Assuming case-insensitive Get
m := user.MapWithOptions(opts)
// m will have lowercase keys: "name", "age"

Performance: If opts is nil or uses all default values, this method delegates to Map() for optimal performance.

Concurrency: MapWithOptions is safe for concurrent use.

func (Result) String

func (r Result) String() string

String returns the string representation of the result. For Null types, it returns an empty string. For Array types, it returns a JSON-like array representation. This implements the fmt.Stringer interface.

Example

ExampleResult_String demonstrates string conversion

xml := `<user>
		<name>John Doe</name>
		<age>30</age>
	</user>`

name := Get(xml, "user.name")
fmt.Println(name.String())
Output:
John Doe

func (Result) Value

func (r Result) Value() interface{}

Value returns the result as an interface{} with the appropriate Go type.

type SegmentType

type SegmentType int

SegmentType represents the type of a path segment.

const (
	// SegmentElement represents an element name in the path.
	SegmentElement SegmentType = iota
	// SegmentAttribute represents an attribute access (@attribute).
	SegmentAttribute
	// SegmentIndex represents an array index access ([n] or .n).
	SegmentIndex
	// SegmentWildcard represents a wildcard match (* or **).
	SegmentWildcard
	// SegmentFilter represents a query filter ([condition]).
	SegmentFilter
	// SegmentText represents text content access (%).
	SegmentText
	// SegmentCount represents array length/count (#).
	SegmentCount
	// SegmentFieldExtraction represents field extraction from all array elements (#.field).
	SegmentFieldExtraction
)

type Type

type Type int

Type represents the type of a Result value.

const (
	// Null represents a non-existent path.
	Null Type = iota
	// String represents a text value.
	String
	// Number represents a numeric value.
	Number
	// True represents a boolean true value.
	True
	// False represents a boolean false value.
	False
	// Element represents an XML element.
	Element
	// Attribute represents an XML attribute.
	Attribute
	// Array represents multiple XML elements with the same name.
	Array
)

type ValidateError

type ValidateError struct {
	Line    int
	Column  int
	Message string
	// Err is the underlying cause, if any. It is a *LimitError when validation
	// failed because a security limit was exceeded.
	Err error
}

ValidateError represents an XML validation error with location information

func ValidateBytesWithError

func ValidateBytesWithError(xml []byte) *ValidateError

ValidateBytesWithError checks XML and returns detailed error (byte slice variant)

func ValidateWithError

func ValidateWithError(xml string) *ValidateError

ValidateWithError checks XML and returns detailed error on failure Returns nil if valid, *ValidateError otherwise

Example

ExampleValidateWithError demonstrates detailed error reporting

// Invalid XML with unclosed tag
xml := `<root><person><name>John</name></root>`

err := ValidateWithError(xml)
if err != nil {
	fmt.Printf("Validation error: %s\n", err.Message)
}
Output:
Validation error: mismatched closing tag 'root' (expected 'person' opened at line 1, column 6)
Example (FragmentWithText)

ExampleValidateWithError_fragmentWithText demonstrates that text between roots is invalid

// Text content between root elements is not allowed
fragment := `<item>first</item>invalid text<item>second</item>`

err := ValidateWithError(fragment)
if err != nil {
	fmt.Println("Text between roots is not allowed")
}
Output:
Text between roots is not allowed

func (*ValidateError) Error

func (e *ValidateError) Error() string

func (*ValidateError) Unwrap added in v0.6.0

func (e *ValidateError) Unwrap() error

Unwrap returns the underlying cause, so that errors.Is(err, ErrLimitExceeded) reports true for validation failures caused by a security limit.

Directories

Path Synopsis
examples
concurrency command
nolint // Example programs don't need package comments
nolint // Example programs don't need package comments
internal
pattern
Package pattern provides wildcard pattern matching with bounded complexity.
Package pattern provides wildcard pattern matching with bounded complexity.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL