19 KiB
YAML LST Structure Reference
Complete reference for OpenRewrite's YAML Lossless Semantic Tree (LST) structure.
Overview
The YAML LST represents YAML documents as a tree structure that preserves all formatting, comments, and whitespace. This allows transformations that maintain the original file's appearance.
Type Hierarchy
org.openrewrite.yaml.tree.Yaml
├── Yaml.Documents (root container)
├── Yaml.Document (single document in multi-doc file)
├── Yaml.Mapping (key-value pairs, similar to JSON object)
│ └── Yaml.Mapping.Entry (single key-value pair)
├── Yaml.Sequence (arrays/lists)
│ └── Yaml.Sequence.Entry (single array item)
├── Yaml.Scalar (primitive values: strings, numbers, booleans)
│ └── Yaml.Scalar.Key (key in a key-value pair)
└── Yaml.Anchor (YAML anchors and aliases)
Core Types
Yaml.Documents
The root element containing one or more YAML documents.
public interface Documents extends Yaml {
List<Document> getDocuments();
Documents withDocuments(List<Document> documents);
}
Usage:
@Override
public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext ctx) {
// Process all documents in the file
List<Yaml.Document> modified = ListUtils.map(
documents.getDocuments(),
doc -> (Yaml.Document) visit(doc, ctx)
);
return documents.withDocuments(modified);
}
Yaml.Document
A single YAML document (files can contain multiple documents separated by ---).
public interface Document extends Yaml {
Block getBlock(); // Root block (usually Mapping or Sequence)
Document withBlock(Block block);
boolean isExplicit(); // True if document starts with ---
}
Usage:
@Override
public Yaml.Document visitDocument(Yaml.Document document, ExecutionContext ctx) {
if (document.getBlock() instanceof Yaml.Mapping) {
Yaml.Mapping root = (Yaml.Mapping) document.getBlock();
// Process root mapping
Yaml.Mapping modified = (Yaml.Mapping) visit(root, ctx);
if (modified != root) {
return document.withBlock(modified);
}
}
return super.visitDocument(document, ctx);
}
Yaml.Mapping
Represents a YAML mapping (key-value pairs), equivalent to JSON objects or Python dictionaries.
public interface Mapping extends Block {
List<Entry> getEntries();
Mapping withEntries(List<Entry> entries);
String getAnchor(); // YAML anchor if present
}
Example YAML:
name: my-workflow
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
Navigation:
@Override
public Yaml.Mapping visitMapping(Yaml.Mapping mapping, ExecutionContext ctx) {
for (Yaml.Mapping.Entry entry : mapping.getEntries()) {
String key = entry.getKey().getValue();
Block value = entry.getValue();
if ("jobs".equals(key) && value instanceof Yaml.Mapping) {
Yaml.Mapping jobsMapping = (Yaml.Mapping) value;
// Process each job
for (Yaml.Mapping.Entry jobEntry : jobsMapping.getEntries()) {
String jobName = jobEntry.getKey().getValue();
// Process job...
}
}
}
return super.visitMapping(mapping, ctx);
}
Yaml.Mapping.Entry
A single key-value pair within a mapping.
public interface Entry extends Yaml {
Yaml.Scalar.Key getKey();
Block getValue(); // Can be Scalar, Mapping, or Sequence
Entry withKey(Yaml.Scalar.Key key);
Entry withValue(Block value);
}
Key Methods:
getKey().getValue()- Get the key as a string (always safe, no cast needed)getValue()- Get the value (requires type check and cast)withKey()- Create new entry with different keywithValue()- Create new entry with different value
Usage:
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
String keyName = entry.getKey().getValue(); // Safe access
// Check value type before processing
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
String value = scalar.getValue();
// Modify value
if ("old-value".equals(value)) {
return entry.withValue(scalar.withValue("new-value"));
}
} else if (entry.getValue() instanceof Yaml.Mapping) {
Yaml.Mapping nested = (Yaml.Mapping) entry.getValue();
// Process nested mapping
} else if (entry.getValue() instanceof Yaml.Sequence) {
Yaml.Sequence sequence = (Yaml.Sequence) entry.getValue();
// Process sequence
}
return super.visitMappingEntry(entry, ctx);
}
Yaml.Sequence
Represents a YAML sequence (array/list).
public interface Sequence extends Block {
List<Entry> getEntries();
Sequence withEntries(List<Entry> entries);
String getAnchor();
}
Example YAML:
branches:
- main
- develop
- feature/*
# Or inline style:
branches: [main, develop, feature/*]
Navigation:
@Override
public Yaml.Sequence visitSequence(Yaml.Sequence sequence, ExecutionContext ctx) {
List<Yaml.Sequence.Entry> entries = sequence.getEntries();
// Iterate through sequence items
for (Yaml.Sequence.Entry entry : entries) {
if (entry.getBlock() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock();
String value = scalar.getValue();
// Process value...
} else if (entry.getBlock() instanceof Yaml.Mapping) {
Yaml.Mapping mapping = (Yaml.Mapping) entry.getBlock();
// Process mapping item...
}
}
return super.visitSequence(sequence, ctx);
}
Adding Items:
@Override
public Yaml.Sequence visitSequence(Yaml.Sequence sequence, ExecutionContext ctx) {
// Check if 'main' branch exists
boolean hasMain = false;
for (Yaml.Sequence.Entry entry : sequence.getEntries()) {
if (entry.getBlock() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock();
if ("main".equals(scalar.getValue())) {
hasMain = true;
break;
}
}
}
if (!hasMain) {
// Create new scalar for 'main'
Yaml.Scalar mainScalar = new Yaml.Scalar(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
Yaml.Scalar.Style.PLAIN,
null,
"main"
);
// Wrap in sequence entry
Yaml.Sequence.Entry mainEntry = new Yaml.Sequence.Entry(
Tree.randomId(),
Space.format("\n - "), // Proper indentation
Markers.EMPTY,
mainScalar,
false
);
// Add to sequence
return sequence.withEntries(
ListUtils.concat(sequence.getEntries(), mainEntry)
);
}
return super.visitSequence(sequence, ctx);
}
Yaml.Sequence.Entry
A single item in a sequence.
public interface Entry extends Yaml {
Block getBlock(); // The actual value
Entry withBlock(Block block);
boolean isTrailingCommaPrefix();
}
Usage:
@Override
public Yaml.Sequence.Entry visitSequenceEntry(Yaml.Sequence.Entry entry, ExecutionContext ctx) {
if (entry.getBlock() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock();
String value = scalar.getValue();
// Update version references
if (value.contains("@v2")) {
String updated = value.replace("@v2", "@v3");
return entry.withBlock(scalar.withValue(updated));
}
}
return super.visitSequenceEntry(entry, ctx);
}
Yaml.Scalar
Represents primitive values (strings, numbers, booleans, null).
public interface Scalar extends Block {
Style getStyle(); // PLAIN, SINGLE_QUOTED, DOUBLE_QUOTED, etc.
String getAnchor();
String getValue();
Scalar withValue(String value);
Scalar withStyle(Style style);
enum Style {
PLAIN,
SINGLE_QUOTED,
DOUBLE_QUOTED,
LITERAL,
FOLDED
}
}
Example YAML:
plain: value
single: 'value'
double: "value"
number: 42
boolean: true
null_value: null
multiline: |
Line 1
Line 2
Usage:
// Reading scalar values
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
String value = scalar.getValue();
Yaml.Scalar.Style style = scalar.getStyle();
// Modify value while preserving style
Yaml.Scalar updated = scalar.withValue("new-value");
return entry.withValue(updated);
}
// Creating new scalars
Yaml.Scalar newScalar = new Yaml.Scalar(
Tree.randomId(), // Unique ID
Space.EMPTY, // Prefix whitespace
Markers.EMPTY, // Markers for search results, etc.
Yaml.Scalar.Style.PLAIN, // Quoting style
null, // Anchor
"value" // Actual value
);
Yaml.Scalar.Key
Special scalar type used for keys in mappings.
public interface Key extends Yaml {
String getValue();
Key withValue(String value);
}
Usage:
// Keys are always accessible via entry.getKey()
String keyName = entry.getKey().getValue(); // No casting needed!
// Rename a key
if ("old-key".equals(entry.getKey().getValue())) {
Yaml.Scalar.Key newKey = entry.getKey().withValue("new-key");
return entry.withKey(newKey);
}
Navigation Patterns
Cursor Navigation
The Cursor provides context about the current position in the tree.
// Get parent elements
Cursor parent = getCursor().getParent();
Cursor grandparent = getCursor().getParent(2);
// Check parent type
if (parent != null && parent.getValue() instanceof Yaml.Mapping) {
Yaml.Mapping parentMapping = (Yaml.Mapping) parent.getValue();
// Process parent...
}
// Get all ancestors of a type
Iterator<Yaml.Mapping> mappings = getCursor().getPathAsStream()
.filter(p -> p instanceof Yaml.Mapping)
.map(p -> (Yaml.Mapping) p)
.iterator();
Finding Siblings
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
// Get parent mapping to access siblings
Cursor parent = getCursor().getParent();
if (parent != null && parent.getValue() instanceof Yaml.Mapping) {
Yaml.Mapping parentMapping = (Yaml.Mapping) parent.getValue();
// Find sibling entries
for (Yaml.Mapping.Entry sibling : parentMapping.getEntries()) {
if (sibling != entry) {
String siblingKey = sibling.getKey().getValue();
// Check sibling...
}
}
}
return super.visitMappingEntry(entry, ctx);
}
Path-Based Navigation with JsonPath
// Match specific paths in YAML structure
JsonPathMatcher jobMatcher = new JsonPathMatcher("$.jobs.*");
JsonPathMatcher stepMatcher = new JsonPathMatcher("$.jobs.*.steps[*]");
JsonPathMatcher usesMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
if (usesMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) {
// This is a 'uses' field within a step
// Process it...
}
return super.visitMappingEntry(entry, ctx);
}
Type Checking and Casting
Safe Type Checking Pattern
Block value = entry.getValue();
if (value instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) value;
String stringValue = scalar.getValue();
// Process scalar...
} else if (value instanceof Yaml.Mapping) {
Yaml.Mapping mapping = (Yaml.Mapping) value;
// Process mapping...
} else if (value instanceof Yaml.Sequence) {
Yaml.Sequence sequence = (Yaml.Sequence) value;
// Process sequence...
} else {
// Handle other types or null
}
Null Safety
// Keys never need null checking (always present)
String key = entry.getKey().getValue(); // Safe
// Values might be null or unexpected types
Block value = entry.getValue();
if (value == null) {
// Handle null value (represents 'key:' with no value)
}
// Scalar values can be null string
if (value instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) value;
String stringValue = scalar.getValue();
if (stringValue == null || "null".equals(stringValue)) {
// Handle YAML null
}
}
Creating New Elements
Creating Scalars
// Plain scalar
Yaml.Scalar plain = new Yaml.Scalar(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
Yaml.Scalar.Style.PLAIN,
null,
"value"
);
// Quoted scalar
Yaml.Scalar quoted = new Yaml.Scalar(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
Yaml.Scalar.Style.DOUBLE_QUOTED,
null,
"value with spaces"
);
Creating Mapping Entries
// Create key
Yaml.Scalar.Key key = new Yaml.Scalar.Key(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
"key-name"
);
// Create value
Yaml.Scalar value = new Yaml.Scalar(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
Yaml.Scalar.Style.PLAIN,
null,
"value"
);
// Create entry
Yaml.Mapping.Entry newEntry = new Yaml.Mapping.Entry(
Tree.randomId(),
Space.format("\n "), // Indentation
Markers.EMPTY,
key,
Space.format(" "), // Space after colon
value
);
Creating Sequences
// Create sequence items
List<Yaml.Sequence.Entry> entries = new ArrayList<>();
entries.add(new Yaml.Sequence.Entry(
Tree.randomId(),
Space.format("\n - "),
Markers.EMPTY,
new Yaml.Scalar(Tree.randomId(), Space.EMPTY, Markers.EMPTY,
Yaml.Scalar.Style.PLAIN, null, "item1"),
false
));
entries.add(new Yaml.Sequence.Entry(
Tree.randomId(),
Space.format("\n - "),
Markers.EMPTY,
new Yaml.Scalar(Tree.randomId(), Space.EMPTY, Markers.EMPTY,
Yaml.Scalar.Style.PLAIN, null, "item2"),
false
));
// Create sequence
Yaml.Sequence sequence = new Yaml.Sequence(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
null, // anchor
entries
);
Common Pitfalls
1. Not Calling Super Methods
// ❌ WRONG - tree traversal stops
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
if ("target".equals(entry.getKey().getValue())) {
return entry.withValue(newValue);
}
return entry; // ❌ Should call super
}
// ✅ CORRECT
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
if ("target".equals(entry.getKey().getValue())) {
return entry.withValue(newValue);
}
return super.visitMappingEntry(entry, ctx); // ✅
}
2. Mutating Instead of Creating New Objects
// ❌ WRONG - LST is immutable
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
scalar.setValue("new-value"); // ❌ This doesn't exist
// ✅ CORRECT
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
Yaml.Scalar updated = scalar.withValue("new-value");
return entry.withValue(updated);
3. Forgetting Type Checks
// ❌ WRONG - may throw ClassCastException
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
// ✅ CORRECT
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
// Process safely
}
4. Incorrect Whitespace/Indentation
// ❌ WRONG - no indentation
Space.EMPTY // Results in key:value on same line as parent
// ✅ CORRECT - proper YAML indentation
Space.format("\n ") // Newline + 2-space indent
5. Not Returning Original When Unchanged
// ❌ WRONG - creates unnecessary tree copies
return entry.withValue(entry.getValue());
// ✅ CORRECT - return original if unchanged
if (shouldModify) {
return entry.withValue(newValue);
}
return super.visitMappingEntry(entry, ctx); // Returns original
Complete Example: Multi-Level Navigation
/**
* Find all GitHub Actions steps using deprecated actions
* and update them to newer versions
*/
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
// Match: $.jobs.*.steps[*].uses
JsonPathMatcher usesMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
if (usesMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) {
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
String actionRef = scalar.getValue();
// Navigate to parent step to check conditions
Cursor stepCursor = getCursor().getParent(2);
if (stepCursor != null && stepCursor.getValue() instanceof Yaml.Mapping) {
Yaml.Mapping step = (Yaml.Mapping) stepCursor.getValue();
// Check if step has 'if' condition
boolean hasCondition = false;
for (Yaml.Mapping.Entry stepEntry : step.getEntries()) {
if ("if".equals(stepEntry.getKey().getValue())) {
hasCondition = true;
break;
}
}
// Only update unconditional steps
if (!hasCondition && actionRef.contains("@v2")) {
String updated = actionRef.replace("@v2", "@v3");
return entry.withValue(scalar.withValue(updated));
}
}
}
}
return super.visitMappingEntry(entry, ctx);
}
Reference Chart
| LST Type | Represents | Common Methods | Notes |
|---|---|---|---|
Documents |
File root | getDocuments() |
Container for multiple docs |
Document |
Single doc | getBlock() |
May have --- separator |
Mapping |
Key-value pairs | getEntries() |
Like JSON object |
Mapping.Entry |
One key-value | getKey(), getValue() |
Basic building block |
Sequence |
Array/list | getEntries() |
Ordered collection |
Sequence.Entry |
Array item | getBlock() |
Wraps actual value |
Scalar |
Primitive value | getValue(), getStyle() |
String, number, bool, null |
Scalar.Key |
Mapping key | getValue() |
Always string, no cast needed |
Additional Resources
- OpenRewrite YAML LST JavaDoc: https://docs.openrewrite.org/reference/yaml-lossless-semantic-trees
- YAML Specification: https://yaml.org/spec/1.2/spec.html
- OpenRewrite Visitor Pattern: https://docs.openrewrite.org/concepts-and-explanations/visitors