Groovy has an optional groovy-yaml module which provides support for converting between Groovy objects and YAML. The classes dedicated to
YAML serialization and parsing are found in the groovy.yaml package.
1. YamlSlurper
YamlSlurper is a class that parses YAML text or reader content into Groovy data structures (objects) such as maps, lists and
primitive types like Integer, Double, Boolean and String.
The class comes with a bunch of overloaded parse methods plus some special methods such as parseText
and others. For the next example we will use the parseText method. It parses a YAML String and recursively converts it to a
list or map of objects. The other parse* methods are similar in that they return a YAML String but for different parameter
types.
def ys = new YamlSlurper()
def yaml = ys.parseText '''
language: groovy
sudo: required
dist: trusty
matrix:
include:
- jdk: openjdk10
- jdk: oraclejdk9
- jdk: oraclejdk8
before_script:
- |
unset _JAVA_OPTIONS
'''
assert 'groovy' == yaml.language
assert 'required' == yaml.sudo
assert 'trusty' == yaml.dist
assert ['openjdk10', 'oraclejdk9', 'oraclejdk8'] == yaml.matrix.include.jdk
assert ['unset _JAVA_OPTIONS'] == yaml.before_script*.trim()
Notice the result is a plain map and can be handled like a normal Groovy object instance. YamlSlurper parses the
given YAML as defined by the YAML Ain’t Markup Language (YAML™).
As YamlSlurper is returning pure Groovy object instances without any special YAML classes in the back, its usage
is transparent. In fact, YamlSlurper results conform to GPath expressions. GPath is a powerful expression language
that is supported by multiple slurpers for different data formats (XmlSlurper for XML being one example).
| For more details please have a look at the section on GPath expressions. |
The following table gives an overview of the YAML types and the corresponding Groovy data types:
| YAML | Groovy |
|---|---|
string |
|
number |
|
object |
|
array |
|
true |
|
false |
|
null |
|
date/time |
|
Whenever a value in YAML is null, YamlSlurper supplements it with the Groovy null value. This is in contrast to other
YAML parsers that represent a null value with a library-provided singleton object.
|
|
For the untyped |
1.1. Typed parsing
YamlSlurper can parse YAML directly into typed objects using Jackson databinding.
Standard Jackson annotations such as @JsonProperty and @JsonFormat are supported
for property mapping and type conversion:
static class ServerConfig {
String host
int port
}
def config = new YamlSlurper().parseTextAs(ServerConfig, '''\
host: localhost
port: 8080
''')
assert config.host == 'localhost'
assert config.port == 8080
For simple cases, Groovy’s as coercion also works with the untyped result:
def config = new YamlSlurper().parseText(yaml) as ServerConfig.
The parseTextAs method uses Jackson databinding, which supports richer annotation-driven
mapping via @JsonProperty, @JsonFormat, etc.
|
1.2. Typed date and time values
When a target type declares java.time.* fields, the typed parseAs/parseTextAs
API and YamlBuilder.toYaml round-trip YAML temporal values with full fidelity,
including the original zone offset for OffsetDateTime:
static class Event {
java.time.OffsetDateTime created
java.time.LocalDate effective
java.time.LocalTime windowStart
java.time.LocalDateTime updated
String name
}
def original = new Event(
created: java.time.OffsetDateTime.parse('1979-05-27T07:32:00-08:00'),
effective: java.time.LocalDate.of(1979, 5, 27),
windowStart: java.time.LocalTime.of(7, 32, 0),
updated: java.time.LocalDateTime.of(1979, 5, 27, 7, 32, 0),
name: 'demo')
def yaml = YamlBuilder.toYaml(original)
def parsed = new YamlSlurper().parseTextAs(Event, yaml)
assert parsed.created == original.created // OffsetDateTime, non-UTC offset preserved
assert parsed.effective == original.effective // LocalDate
assert parsed.windowStart == original.windowStart // LocalTime
assert parsed.updated == original.updated // LocalDateTime
assert parsed.name == original.name
1.3. Anchors and aliases
YamlSlurper does not resolve YAML anchors and aliases. An alias arrives as a plain
string holding the anchor’s name, and a merge key (<<) is left as a literal entry
rather than merging anything:
def yaml = '''\
defaults: &d
host: example.com
port: 443
prod: *d
'''.stripIndent()
def loaded = new YamlSlurper().parseText(yaml)
assert loaded.defaults == [host: 'example.com', port: 443]
assert loaded.prod == 'd' // the anchor's name, not the mapping it names
def merged = new YamlSlurper().parseText('''\
defaults: &d
a: 1
b: 9
prod:
<<: *d
b: 2
'''.stripIndent())
assert merged.prod == ['<<': 'd', b: 2] // no merge occurs
This comes from the Jackson YAML backend the slurper is built on, which reads the document as a stream of events and does not replay an anchored subtree when an alias refers to it. It is a long-standing, deliberate limitation (jackson-dataformats-text#98, closed as not planned), so a document that relies on anchors should either be flattened before parsing, or read as shown below.
For the typed parseAs API only, Jackson’s own object-identity mechanism can stand in
for aliases. Annotate the referenced class with @JsonIdentityInfo and an anchored
mapping round-trips as one shared instance:
@JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator, property = '@id')
static class Address {
String street
String city
}
static class Shipment {
Address billTo
Address shipTo
}
def shipment = new YamlSlurper().parseTextAs(Shipment, '''\
billTo: &id001
street: 123 Tornado Alley
city: East Centerville
shipTo: *id001
'''.stripIndent())
assert shipment.shipTo.street == '123 Tornado Alley'
assert shipment.billTo.is(shipment.shipTo) // one shared instance
This works for mutable, setter-based classes only. A type deserialized through a
constructor — @JsonCreator, and immutable classes generally — fails with
No Object Id found even for a plain backward reference, so do not reach for this
with @Immutable targets. It is Jackson’s object-identity feature standing in for
aliases, not alias support.
1.4. Resolving aliases with SnakeYAML directly
The SnakeYAML library resolves anchors, aliases, and merge keys in full. This is not
part of groovy-yaml — it is a second YAML parser used directly, shown here because
the need is common. Today SnakeYAML happens to reach the classpath transitively via
the Jackson YAML backend; that arrangement is not yours to rely on (and ends with
Jackson 3), so declare org.yaml:snakeyaml as a dependency if you use this.
def yaml = new Yaml(new SafeConstructor(new LoaderOptions()))
def loaded = yaml.load('''\
defaults: &d
a: 1
b: 9
prod:
<<: *d
b: 2
again: *d
'''.stripIndent())
assert loaded.prod == [a: 1, b: 2] // merge keys apply
assert loaded.again == [a: 1, b: 9] // aliases resolve
assert loaded.again.is(loaded.defaults) // to the same object
SafeConstructor matters: it produces only maps, lists, and scalars, and refuses the
tagged-type instantiation that makes careless YAML loading dangerous. Differences from
YamlSlurper to be aware of:
-
it follows YAML 1.1 typing, so an untagged timestamp becomes a
java.util.DatewhereYamlSlurperreturns aString; -
input is capped at 3MB of code points by default, and a document may use at most 50 aliases of non-scalar nodes — both raised via
LoaderOptionsif needed, and the alias cap is what keeps an alias-expansion document from being a denial of service; -
a self-referential anchor constructs a genuinely cyclic object graph, which naive traversal —
toString(),JsonOutput.toJson— will not survive.
1.5. Builders
Another way to create YAML from Groovy is to use YamlBuilder. The builder provide a
DSL which allows to formulate an object graph which is then converted to YAML.
def builder = new YamlBuilder()
builder.records {
car {
name 'HSV Maloo'
make 'Holden'
year 2006
country 'Australia'
homepage new URL('http://example.org')
record {
type 'speed'
description 'production pickup truck with speed of 271kph'
}
}
}
assert builder.toString() == '''---
records:
car:
name: "HSV Maloo"
make: "Holden"
year: 2006
country: "Australia"
homepage: "http://example.org"
record:
type: "speed"
description: "production pickup truck with speed of 271kph"
'''
1.6. Typed writing
YamlBuilder.toYaml(object) serializes a typed object directly to YAML using Jackson databinding:
static class ServerConfig {
String host
int port
}
@Test
void testToYaml() {
def config = new ServerConfig(host: 'localhost', port: 8080)
def yaml = YamlBuilder.toYaml(config)
assert yaml.contains('host:')
assert yaml.contains('localhost')
assert yaml.contains('port:')
assert yaml.contains('8080')
}