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 ¶
- Constants
- Variables
- func Delete(xml, path string) (string, error)
- func DeleteBytes(xml []byte, path string) ([]byte, error)
- func DeleteBytesWithOptions(xml []byte, path string, opts *Options) (out []byte, err error)
- func DeleteMany(xml string, paths ...string) (string, error)
- func DeleteManyBytes(xml []byte, paths ...string) ([]byte, error)
- func RegisterModifier(name string, m Modifier) error
- func Set(xml, path string, value interface{}) (string, error)
- func SetBytes(xml []byte, path string, value interface{}) ([]byte, error)
- func SetBytesWithOptions(xml []byte, path string, value interface{}, opts *Options) (out []byte, err error)
- func SetMany(xml string, paths []string, values []interface{}) (string, error)
- func SetManyBytes(xml []byte, paths []string, values []interface{}) ([]byte, error)
- func SetRaw(xml, path, rawxml string) (string, error)
- func SetWithOptions(xml, path string, value interface{}, opts *Options) (string, error)
- func UnregisterModifier(name string) error
- func Valid(xml string) bool
- func ValidBytes(xml []byte) bool
- type Filter
- type FilterOp
- type IndexIntent
- type LimitError
- type Modifier
- type ModifierFunc
- type Options
- type PathSegment
- type Result
- func Get(xml, path string) Result
- func GetBytes(xml []byte, path string) (result Result)
- func GetBytesWithOptions(xml []byte, path string, opts *Options) (result Result)
- func GetMany(xml string, paths ...string) []Result
- func GetString(xml string, path string) Result
- func GetStringWithOptions(xml string, path string, opts *Options) Result
- func GetWithOptions(xml, path string, opts *Options) Result
- func (r Result) Array() []Result
- func (r Result) Bool() bool
- func (r Result) Exists() bool
- func (r Result) Float() float64
- func (r Result) ForEach(iterator func(index int, value Result) bool)
- func (r Result) Get(path string) Result
- func (r Result) GetMany(paths ...string) []Result
- func (r Result) GetWithOptions(path string, opts *Options) Result
- func (r Result) Int() int64
- func (r Result) IsArray() bool
- func (r Result) Map() map[string]Result
- func (r Result) MapWithOptions(opts *Options) map[string]Result
- func (r Result) String() string
- func (r Result) Value() interface{}
- type SegmentType
- type Type
- type ValidateError
Examples ¶
- Package (AttributeFilter)
- Package (NumericFilter)
- Package (RecursiveWildcard)
- Package (SingleLevelWildcard)
- Delete
- DeleteMany
- Get
- Get (ArrayCount)
- Get (ArrayIndex)
- Get (Attribute)
- Get (Filter)
- Get (Modifier)
- Get (ModifierChain)
- Get (RecursiveWildcard)
- Get (TextContent)
- Get (Wildcard)
- GetMany
- GetWithOptions
- GetWithOptions (CaseInsensitive)
- ModifierFunc
- RegisterModifier
- Result.Array
- Result.Bool
- Result.Exists
- Result.Float
- Result.ForEach
- Result.Get
- Result.Get (Array)
- Result.Get (Chaining)
- Result.GetMany
- Result.GetWithOptions
- Result.Int
- Result.Map
- Result.String
- Set
- Set (Attribute)
- Set (CreateNew)
- SetMany
- SetWithOptions
- SetWithOptions (Indent)
- Valid
- Valid (Fragment)
- Valid (FragmentWithProlog)
- ValidateWithError
- ValidateWithError (FragmentWithText)
Constants ¶
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 ¶
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") )
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.
var ErrLimitExceeded = errors.New("security limit exceeded")
ErrLimitExceeded is the sentinel error wrapped by every *LimitError.
Functions ¶
func Delete ¶
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 ¶
DeleteBytes is like Delete but accepts and returns xml as byte slices.
func DeleteBytesWithOptions ¶
DeleteBytesWithOptions is like DeleteBytes but accepts Options for behavioral control. This is used internally by SetBytesWithOptions when value is nil.
func DeleteMany ¶
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 ¶
DeleteManyBytes is like DeleteMany but accepts and returns xml as byte slices for efficiency.
func RegisterModifier ¶
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 ¶
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 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 ¶
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 ¶
SetManyBytes is like SetMany but accepts and returns xml as byte slices for efficiency.
func SetRaw ¶
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 ¶
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 ¶
UnregisterModifier removes a custom modifier from the registry. Built-in modifiers cannot be unregistered (returns error).
func Valid ¶
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 ¶
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")
- Path "items.item.@price" resolves to Result (array of prices)
- @sort modifier sorts the array
- @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 ¶
GetModifier retrieves a registered modifier by name. Returns nil if not found.
func NewModifierFunc ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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
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
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 ¶
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) Map ¶ added in v0.5.0
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
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 ¶
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
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.
Source Files
¶
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. |