or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

basic-operations.mdcustom-serialization.mdindex.mdnode-manipulation.mdstreaming-operations.md
tile.json

node-manipulation.mddocs/

Node Manipulation

Low-level AST (Abstract Syntax Tree) manipulation for fine-grained control over YAML document structure, including comments, anchors, aliases, and formatting. Use Node when you need to preserve or manipulate YAML structure beyond what basic marshaling/unmarshaling provides.

Node

Represents an element in the YAML document hierarchy. Provides intermediate representation that allows detailed control over content being decoded or encoded.

type Node struct {
    Kind        Kind
    Style       Style
    Tag         string
    Value       string
    Anchor      string
    Alias       *Node
    Content     []*Node
    HeadComment string
    LineComment string
    FootComment string
    Line        int
    Column      int
}

Fields

Kind (Kind)

Defines whether the node is a document, mapping, sequence, scalar value, or alias to another node. See Kind type below.

Style (Style)

Allows customizing the appearance of the node in the tree (quoted strings, flow style, etc.). See Style type below.

Tag (string)

YAML tag defining the data type for the value. When decoding, this field is always set to the resolved tag even if not explicitly provided. When encoding, the tag is serialized only if TaggedStyle is used or the implicit tag diverges from the provided one.

Value (string)

Unescaped and unquoted representation of the value (for scalar nodes only).

Anchor (string)

Anchor name for this node, which allows aliases to point to it.

Alias (*Node)

The node that this alias points to. Only valid when Kind is AliasNode.

Content ([]*Node)

Contains child nodes for documents, mappings, and sequences. For mappings, nodes alternate between keys and values.

HeadComment (string)

Comments in the lines preceding the node, not separated by an empty line.

LineComment (string)

Comments at the end of the line where the node is located.

FootComment (string)

Comments following the node and before empty lines.

Line (int)

Line number in the decoded YAML text where the node appears. Not respected when encoding.

Column (int)

Column number in the decoded YAML text where the node appears. Not respected when encoding.

Node Methods

Decode

Decodes the node and stores its data into the value pointed to by v.

func (n *Node) Decode(v interface{}) (err error)
Parameters
  • v (interface{}) - Pointer to the variable where decoded values will be stored
Returns
  • err (error) - Returns *yaml.TypeError if type mismatches occur, nil on success

See Basic YAML Operations for details on type conversion.

Encode

Encodes value v and stores its representation in the node.

func (n *Node) Encode(v interface{}) (err error)
Parameters
  • v (interface{}) - The value to encode into the node
Returns
  • err (error) - Error if encoding fails, nil otherwise

IsZero

Returns whether the node has all of its fields unset.

func (n *Node) IsZero() bool
Returns
  • bool - True if all fields are unset, false otherwise

LongTag

Returns the long form of the tag that indicates the data type for the node.

func (n *Node) LongTag() string
Returns
  • string - Long form tag (e.g., "tag:yaml.org,2002:str")

If the Tag field isn't explicitly defined, one will be computed based on the node properties.

ShortTag

Returns the short form of the YAML tag that indicates data type for the node.

func (n *Node) ShortTag() string
Returns
  • string - Short form tag (e.g., "!!str")

If the Tag field isn't explicitly defined, one will be computed based on the node properties.

SetString

Convenience function that sets the node to a string value and defines its style appropriately.

func (n *Node) SetString(s string)
Parameters
  • s (string) - The string value to set
Behavior
  • Sets Kind to ScalarNode
  • If string is valid UTF-8, sets Tag to strTag
  • If string is not valid UTF-8, encodes as base64 and sets Tag to binaryTag
  • If string contains newlines, sets Style to LiteralStyle

Supporting Types

Kind

Represents the type of YAML node.

type Kind uint32

const (
    DocumentNode Kind = 1 << iota
    SequenceNode
    MappingNode
    ScalarNode
    AliasNode
)

Constants

  • DocumentNode - Represents a YAML document root
  • SequenceNode - Represents a YAML sequence (array/list)
  • MappingNode - Represents a YAML mapping (object/dictionary)
  • ScalarNode - Represents a scalar value (string, number, bool, etc.)
  • AliasNode - Represents an alias to another node (using anchors)

Style

Controls the rendering style of YAML nodes.

type Style uint32

const (
    TaggedStyle Style = 1 << iota
    DoubleQuotedStyle
    SingleQuotedStyle
    LiteralStyle
    FoldedStyle
    FlowStyle
)

Constants

  • TaggedStyle - Explicitly include the type tag in output
  • DoubleQuotedStyle - Use double quotes for strings
  • SingleQuotedStyle - Use single quotes for strings
  • LiteralStyle - Use literal block scalar (preserves newlines with |)
  • FoldedStyle - Use folded block scalar (folds newlines with >)
  • FlowStyle - Use flow style (inline/compact format)

Usage Examples

Reading YAML with Comments

package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
)

func main() {
    data := []byte(`
# This is a head comment
name: MyApp  # This is a line comment
version: 1
# This is a foot comment
`)

    var node yaml.Node
    err := yaml.Unmarshal(data, &node)
    if err != nil {
        panic(err)
    }

    // Navigate to the document content (root mapping)
    doc := node.Content[0]

    // Iterate through mapping key-value pairs
    for i := 0; i < len(doc.Content); i += 2 {
        key := doc.Content[i]
        value := doc.Content[i+1]

        fmt.Printf("Key: %s\n", key.Value)
        fmt.Printf("Value: %s\n", value.Value)
        fmt.Printf("HeadComment: %q\n", key.HeadComment)
        fmt.Printf("LineComment: %q\n", value.LineComment)
        fmt.Printf("FootComment: %q\n", value.FootComment)
    }
}

Modifying YAML Structure

func AddField(data []byte, key, value string) ([]byte, error) {
    var node yaml.Node
    err := yaml.Unmarshal(data, &node)
    if err != nil {
        return nil, err
    }

    // Get the root mapping
    root := node.Content[0]

    // Create key node
    keyNode := &yaml.Node{
        Kind:  yaml.ScalarNode,
        Value: key,
    }

    // Create value node
    valueNode := &yaml.Node{
        Kind:  yaml.ScalarNode,
        Value: value,
    }

    // Add to content
    root.Content = append(root.Content, keyNode, valueNode)

    // Marshal back to YAML
    return yaml.Marshal(&node)
}

Creating YAML with Custom Style

func CreateFlowStyleYAML() ([]byte, error) {
    // Create a mapping node with flow style
    node := yaml.Node{
        Kind: yaml.DocumentNode,
        Content: []*yaml.Node{
            {
                Kind:  yaml.MappingNode,
                Style: yaml.FlowStyle,
                Content: []*yaml.Node{
                    {Kind: yaml.ScalarNode, Value: "name"},
                    {Kind: yaml.ScalarNode, Value: "MyApp"},
                    {Kind: yaml.ScalarNode, Value: "items"},
                    {
                        Kind:  yaml.SequenceNode,
                        Style: yaml.FlowStyle,
                        Content: []*yaml.Node{
                            {Kind: yaml.ScalarNode, Value: "item1"},
                            {Kind: yaml.ScalarNode, Value: "item2"},
                            {Kind: yaml.ScalarNode, Value: "item3"},
                        },
                    },
                },
            },
        },
    }

    return yaml.Marshal(&node)
    // Output: {name: MyApp, items: [item1, item2, item3]}
}

Preserving and Adding Comments

func AddCommentToField(data []byte, fieldName, comment string) ([]byte, error) {
    var node yaml.Node
    err := yaml.Unmarshal(data, &node)
    if err != nil {
        return nil, err
    }

    // Get the root mapping
    root := node.Content[0]

    // Find the field and add comment
    for i := 0; i < len(root.Content); i += 2 {
        key := root.Content[i]
        value := root.Content[i+1]

        if key.Value == fieldName {
            value.LineComment = comment
            break
        }
    }

    return yaml.Marshal(&node)
}

// Usage:
data := []byte("name: MyApp\nversion: 1")
result, _ := AddCommentToField(data, "version", "Major version")
fmt.Printf("%s", result)
// Output:
// name: MyApp
// version: 1 # Major version

Working with Anchors and Aliases

func CreateYAMLWithAnchors() ([]byte, error) {
    // Create a node with an anchor
    defaultConfig := &yaml.Node{
        Kind:   yaml.MappingNode,
        Anchor: "default",
        Content: []*yaml.Node{
            {Kind: yaml.ScalarNode, Value: "timeout"},
            {Kind: yaml.ScalarNode, Value: "30"},
            {Kind: yaml.ScalarNode, Value: "retries"},
            {Kind: yaml.ScalarNode, Value: "3"},
        },
    }

    // Create an alias to the anchor
    alias := &yaml.Node{
        Kind:  yaml.AliasNode,
        Alias: defaultConfig,
    }

    // Build document
    doc := yaml.Node{
        Kind: yaml.DocumentNode,
        Content: []*yaml.Node{
            {
                Kind: yaml.MappingNode,
                Content: []*yaml.Node{
                    {Kind: yaml.ScalarNode, Value: "default"},
                    defaultConfig,
                    {Kind: yaml.ScalarNode, Value: "production"},
                    alias,
                },
            },
        },
    }

    return yaml.Marshal(&doc)
    // Output:
    // default: &default
    //   timeout: 30
    //   retries: 3
    // production: *default
}

Unmarshaling into Node

You can unmarshal directly into a Node to work with the AST:

package main

import (
    "fmt"
    "gopkg.in/yaml.v3"
)

func main() {
    data := []byte(`
name: MyApp
version: 1
config:
  debug: true
  port: 8080
`)

    var node yaml.Node
    err := yaml.Unmarshal(data, &node)
    if err != nil {
        panic(err)
    }

    // node.Kind == DocumentNode
    // node.Content[0].Kind == MappingNode
    // Navigate the tree structure
    walkNode(&node, 0)
}

func walkNode(node *yaml.Node, depth int) {
    indent := ""
    for i := 0; i < depth; i++ {
        indent += "  "
    }

    fmt.Printf("%sKind: %d, Value: %q\n", indent, node.Kind, node.Value)

    for _, child := range node.Content {
        walkNode(child, depth+1)
    }
}

Mixed Usage: Node with Regular Types

You can embed Node fields within regular structs:

type Config struct {
    Name    string
    Version int
    // Store the raw YAML node for the metadata section
    Metadata yaml.Node `yaml:"metadata"`
}

data := []byte(`
name: MyApp
version: 1
metadata:
  author: Alice
  tags:
    - production
    - stable
`)

var config Config
yaml.Unmarshal(data, &config)

// Access regular fields
fmt.Println(config.Name)    // "MyApp"
fmt.Println(config.Version) // 1

// Work with the metadata node
var meta map[string]interface{}
config.Metadata.Decode(&meta)
fmt.Println(meta) // map[author:Alice tags:[production stable]]

Common Patterns

Merging YAML Documents

func MergeYAML(base, override []byte) ([]byte, error) {
    var baseNode, overrideNode yaml.Node

    if err := yaml.Unmarshal(base, &baseNode); err != nil {
        return nil, err
    }
    if err := yaml.Unmarshal(override, &overrideNode); err != nil {
        return nil, err
    }

    // Merge logic: add override fields to base
    baseRoot := baseNode.Content[0]
    overrideRoot := overrideNode.Content[0]

    baseMap := make(map[string]int)
    for i := 0; i < len(baseRoot.Content); i += 2 {
        key := baseRoot.Content[i].Value
        baseMap[key] = i
    }

    for i := 0; i < len(overrideRoot.Content); i += 2 {
        key := overrideRoot.Content[i]
        value := overrideRoot.Content[i+1]

        if idx, exists := baseMap[key.Value]; exists {
            // Replace existing value
            baseRoot.Content[idx+1] = value
        } else {
            // Add new key-value pair
            baseRoot.Content = append(baseRoot.Content, key, value)
        }
    }

    return yaml.Marshal(&baseNode)
}

Filtering YAML Fields

func RemoveFields(data []byte, fieldsToRemove []string) ([]byte, error) {
    var node yaml.Node
    if err := yaml.Unmarshal(data, &node); err != nil {
        return nil, err
    }

    root := node.Content[0]
    removeSet := make(map[string]bool)
    for _, f := range fieldsToRemove {
        removeSet[f] = true
    }

    newContent := []*yaml.Node{}
    for i := 0; i < len(root.Content); i += 2 {
        key := root.Content[i]
        value := root.Content[i+1]

        if !removeSet[key.Value] {
            newContent = append(newContent, key, value)
        }
    }

    root.Content = newContent
    return yaml.Marshal(&node)
}

Notes

  • Position Information: Line and Column fields are populated during unmarshaling but are not used during marshaling
  • Comment Preservation: Comments are preserved when unmarshaling into Node and re-marshaling, though exact formatting may change
  • Content Structure: For mappings, Content alternates between key and value nodes. For sequences, each element is a separate node.
  • For simple marshaling/unmarshaling without AST manipulation, use Basic YAML Operations
  • For streaming operations, use Streaming YAML Operations