An open API service indexing awesome lists of open source software.

https://github.com/netascode/xmldot

High-performance Go library for querying and manipulating XML using GJSON-inspired path syntax. Zero dependencies, blazing fast, security-first design.
https://github.com/netascode/xmldot

golang xml xml-parser

Last synced: 8 months ago
JSON representation

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

Awesome Lists containing this project

README

          



xmldot logo


xmldot


GoDoc
Go Report Card
codecov
Playground

get and set xml values with dot notation

XMLDOT is a Go package that provides a [fast](docs/performance.md) and [simple](#get-a-value) way to get and set values in XML documents. It has features such as [dot notation paths](#path-syntax), [wildcards](#wildcards), [filters](#filters), [modifiers](#modifiers), and [array operations](#working-with-arrays).

Inspired by [GJSON](https://github.com/tidwall/gjson) and [SJSON](https://github.com/tidwall/sjson) for JSON.

Getting Started
===============

## Installing

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

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

This will retrieve the library.

## Try It Online

**[🎮 Interactive Playground](https://netascode.github.io/xmldot-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.

```go
package main

import "github.com/netascode/xmldot"

const xml = `


The Go Programming Language
Alan Donovan
44.99

`

func main() {
title := xmldot.Get(xml, "catalog.book.title")
println(title.String())
}
```

This will print:

```
The Go Programming Language
```

## Fluent API (v0.2.0+)

The fluent API enables method chaining on Result objects for cleaner, more readable code:

```go
// 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()
```

**Performance**: Fluent chaining adds ~280% overhead for 3-level chains compared to full paths. For performance-critical code, use direct paths:

```go
// 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:

```go
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".

```go
package main

import "github.com/netascode/xmldot"

const xml = `


The Go Programming Language
44.99

`

func main() {
value, _ := xmldot.Set(xml, "catalog.book.price", 39.99)
println(value)
}
```

This will print:

```xml
The Go Programming Language39.99
```

### Creating Elements with Attributes (v0.3.0+)

Starting from v0.3.0, Set automatically creates missing parent elements when setting attributes. This makes it easy to add attributes to elements that don't yet exist:

```go
xml := ``

// Automatically creates element with id attribute
result, _ := xmldot.Set(xml, "root.user.@id", "123")
// Result:

// Works with deep paths too
result, _ = xmldot.Set(xml, "root.company.department.@name", "Engineering")
// Result:
```

Before v0.3.0, setting an attribute on a non-existent element would return an error. Now the element is automatically created.

## Path Syntax

A path is a series of keys separated by a dot. The dot character can be escaped with `\`.

```xml


The Go Programming Language
Alan Donovan
44.99

programming
go



Learning Go
Jon Bodner
39.99

```

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

### 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:

```xml


The Go Programming Language
44.99


Learning Go
39.99

```

```
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:

```xml


The Go Programming Language
44.99


Learning Go
39.99


Old Book
19.99

```

```
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:

```go
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:

```go
result := xmldot.Get(xml, "catalog.book.title")
for _, title := range result.Array() {
println(title.String())
}
```

Or use `ForEach`:

```go
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:

```go
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.Uint() uint64
result.Float() float64
result.Array() []Result
result.Map() map[string]Result
result.Exists() bool
result.Get(path string) Result
result.ForEach(iterator func(key, value Result) bool)
```

## Namespaces

Basic namespace prefix matching is supported:

```xml

value

```

```
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:

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

## Multiple Paths

Get multiple paths efficiently:

```go
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:

```go
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:

```go
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`:

```go
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:

```go
// 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:

```go
var mu sync.Mutex
currentXML := ""

func updateXML(path string, value interface{}) {
mu.Lock()
defer mu.Unlock()
result, _ := xmldot.Set(currentXML, path, value)
currentXML = result
}
```

See the [Concurrency Guide](docs/concurrency.md) for patterns and best practices.

## Documentation

### Guides

- [Path Syntax Reference](docs/path-syntax.md) - Complete path expression guide
- [Error Handling Guide](docs/error-handling.md) - Error types and patterns
- [Performance Guide](docs/performance.md) - Optimization techniques
- [Concurrency Guide](docs/concurrency.md) - Thread-safety patterns
- [Security Guide](docs/security.md) - Security features and limits
- [Migration Guide](docs/migration.md) - Moving from other libraries

### Examples

- [Basic Get Queries](examples/basic-get/) - Simple element access
- [Basic Set Operations](examples/basic-set/) - Modify XML documents
- [Array Manipulation](examples/arrays/) - Working with arrays
- [Query Filters](examples/filters/) - Filter elements by conditions
- [Result Modifiers](examples/modifiers/) - Transform query results
- [Custom Modifiers](examples/custom-modifiers/) - Build custom transformations
- [Namespace Support](examples/namespaces/) - Work with XML namespaces
- [Performance](examples/performance/) - Optimize for speed

### Real-World Examples

- [RSS Parser](examples/real-world/rss-parser/) - Parse RSS/Atom feeds
- [Config Files](examples/real-world/config-files/) - Manage XML config files
- [SOAP Client](examples/real-world/soap-client/) - Build SOAP clients

### API Documentation

Full API reference available at [GoDoc](https://godoc.org/github.com/netascode/xmldot).

## Contributing

We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Acknowledgments

Inspired by the excellent [gjson](https://github.com/tidwall/gjson) and [sjson](https://github.com/tidwall/sjson) libraries for JSON.