Skip to content

High-performance Go library for querying and manipulating XML using GJSON-inspired path syntax. Zero dependencies, blazing fast, security-first design.

License

Notifications You must be signed in to change notification settings

netascode/xmldot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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.

About

High-performance Go library for querying and manipulating XML using GJSON-inspired path syntax. Zero dependencies, blazing fast, security-first design.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages