This is an automated email from the ASF dual-hosted git repository. paulk pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 29ccde6ef4574deec1ab3bb543bf3cd81108145c Author: Paul King <[email protected]> AuthorDate: Sun Apr 12 15:55:14 2026 +1000 GROOVY-11926: Improve consistency of YAML functionality --- .../src/main/java/groovy/yaml/YamlBuilder.java | 21 ++++++ .../src/main/java/groovy/yaml/YamlSlurper.java | 75 ++++++++++++++++++++++ .../groovy-yaml/src/spec/doc/yaml-userguide.adoc | 25 ++++++++ .../spec/test/groovy/yaml/YamlBuilderTest.groovy | 24 +++++++ .../spec/test/groovy/yaml/YamlParserTest.groovy | 48 ++++++++++++++ 5 files changed, 193 insertions(+) diff --git a/subprojects/groovy-yaml/src/main/java/groovy/yaml/YamlBuilder.java b/subprojects/groovy-yaml/src/main/java/groovy/yaml/YamlBuilder.java index a246c0b3cd..b5fb333b1a 100644 --- a/subprojects/groovy-yaml/src/main/java/groovy/yaml/YamlBuilder.java +++ b/subprojects/groovy-yaml/src/main/java/groovy/yaml/YamlBuilder.java @@ -18,6 +18,9 @@ */ package groovy.yaml; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; import groovy.json.JsonBuilder; import groovy.lang.Closure; import groovy.lang.GroovyObjectSupport; @@ -43,6 +46,24 @@ public class YamlBuilder extends GroovyObjectSupport implements Writable { this.jsonBuilder = new JsonBuilder(); } + /** + * Serializes a typed object to a YAML string using Jackson databinding. + * Supports {@code @JsonProperty} and {@code @JsonFormat} annotations. + * + * @param object the object to serialize + * @return the YAML string + * @since 6.0.0 + */ + public static String toYaml(Object object) { + try { + return new ObjectMapper(new YAMLFactory() + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)) + .writeValueAsString(object); + } catch (IOException e) { + throw new YamlRuntimeException(e); + } + } + public Object getContent() { return jsonBuilder.getContent(); } diff --git a/subprojects/groovy-yaml/src/main/java/groovy/yaml/YamlSlurper.java b/subprojects/groovy-yaml/src/main/java/groovy/yaml/YamlSlurper.java index 39a5165e34..6beeb4d3d0 100644 --- a/subprojects/groovy-yaml/src/main/java/groovy/yaml/YamlSlurper.java +++ b/subprojects/groovy-yaml/src/main/java/groovy/yaml/YamlSlurper.java @@ -18,9 +18,13 @@ */ package groovy.yaml; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import groovy.json.JsonSlurper; +import groovy.yaml.YamlRuntimeException; import org.apache.groovy.yaml.util.YamlConverter; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -91,4 +95,75 @@ public class YamlSlurper { // note: convert to an input stream to allow the support of foreign file objects return parse(Files.newInputStream(path)); } + + /** + * Parse the content of the specified YAML text into a typed object using Jackson databinding. + * Supports {@code @JsonProperty} and {@code @JsonFormat} annotations for + * property mapping and type conversion. + * + * @param type the target type + * @param yaml the content of YAML + * @param <T> the target type + * @return a typed object + * @since 6.0.0 + */ + public <T> T parseTextAs(Class<T> type, String yaml) { + return parseAs(type, new StringReader(yaml)); + } + + /** + * Parse YAML from a reader into a typed object. + * + * @param type the target type + * @param reader the reader of YAML + * @param <T> the target type + * @return a typed object + * @since 6.0.0 + */ + public <T> T parseAs(Class<T> type, Reader reader) { + try (Reader r = reader) { + return new ObjectMapper(new YAMLFactory()).readValue(r, type); + } catch (IOException e) { + throw new YamlRuntimeException(e); + } + } + + /** + * Parse YAML from an input stream into a typed object. + * + * @param type the target type + * @param stream the input stream of YAML + * @param <T> the target type + * @return a typed object + * @since 6.0.0 + */ + public <T> T parseAs(Class<T> type, InputStream stream) { + return parseAs(type, new InputStreamReader(stream)); + } + + /** + * Parse YAML from a file into a typed object. + * + * @param type the target type + * @param file the YAML file + * @param <T> the target type + * @return a typed object + * @since 6.0.0 + */ + public <T> T parseAs(Class<T> type, File file) throws IOException { + return parseAs(type, file.toPath()); + } + + /** + * Parse YAML from a path into a typed object. + * + * @param type the target type + * @param path the path to the YAML file + * @param <T> the target type + * @return a typed object + * @since 6.0.0 + */ + public <T> T parseAs(Class<T> type, Path path) throws IOException { + return parseAs(type, Files.newInputStream(path)); + } } diff --git a/subprojects/groovy-yaml/src/spec/doc/yaml-userguide.adoc b/subprojects/groovy-yaml/src/spec/doc/yaml-userguide.adoc index 1d40327d2e..1334546019 100644 --- a/subprojects/groovy-yaml/src/spec/doc/yaml-userguide.adoc +++ b/subprojects/groovy-yaml/src/spec/doc/yaml-userguide.adoc @@ -90,6 +90,22 @@ The following table gives an overview of the YAML types and the corresponding Gr 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. +=== 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: + +[source,groovy] +---- +include::../test/groovy/yaml/YamlParserTest.groovy[tags=typed_class,indent=0] +---- + +[source,groovy] +---- +include::../test/groovy/yaml/YamlParserTest.groovy[tags=typed_parsing,indent=0] +---- + === Builders Another way to create YAML from Groovy is to use `YamlBuilder`. The builder provide a @@ -99,3 +115,12 @@ DSL which allows to formulate an object graph which is then converted to YAML. ---- include::../test/groovy/yaml/YamlBuilderTest.groovy[tags=build_text,indent=0] ---- + +=== Typed writing + +`YamlBuilder.toYaml(object)` serializes a typed object directly to YAML using Jackson databinding: + +[source,groovy] +---- +include::../test/groovy/yaml/YamlBuilderTest.groovy[tags=typed_writing,indent=0] +---- diff --git a/subprojects/groovy-yaml/src/spec/test/groovy/yaml/YamlBuilderTest.groovy b/subprojects/groovy-yaml/src/spec/test/groovy/yaml/YamlBuilderTest.groovy index 4d0602d7a4..8dbb5efe6c 100644 --- a/subprojects/groovy-yaml/src/spec/test/groovy/yaml/YamlBuilderTest.groovy +++ b/subprojects/groovy-yaml/src/spec/test/groovy/yaml/YamlBuilderTest.groovy @@ -53,4 +53,28 @@ records: ''' // end::build_text[] } + + // tag::typed_writing[] + static class ServerConfig { + String host + int port + } + + 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') + } + // end::typed_writing[] + + void testTypedRoundTrip() { + def original = new ServerConfig(host: 'example.com', port: 443) + def yaml = YamlBuilder.toYaml(original) + def parsed = new YamlSlurper().parseTextAs(ServerConfig, yaml) + assert parsed.host == 'example.com' + assert parsed.port == 443 + } } \ No newline at end of file diff --git a/subprojects/groovy-yaml/src/spec/test/groovy/yaml/YamlParserTest.groovy b/subprojects/groovy-yaml/src/spec/test/groovy/yaml/YamlParserTest.groovy index b410bf10bc..6f289d7fca 100644 --- a/subprojects/groovy-yaml/src/spec/test/groovy/yaml/YamlParserTest.groovy +++ b/subprojects/groovy-yaml/src/spec/test/groovy/yaml/YamlParserTest.groovy @@ -106,6 +106,54 @@ matrix: } + // tag::typed_class[] + static class ServerConfig { + String host + int port + } + // end::typed_class[] + + void testParseTextAs() { + // tag::typed_parsing[] + def config = new YamlSlurper().parseTextAs(ServerConfig, '''\ +host: localhost +port: 8080 +''') + assert config.host == 'localhost' + assert config.port == 8080 + // end::typed_parsing[] + } + + void testParseAsFromReader() { + def reader = new StringReader('host: localhost\nport: 8080') + def config = new YamlSlurper().parseAs(ServerConfig, reader) + assert config instanceof ServerConfig + assert config.host == 'localhost' + assert config.port == 8080 + } + + void testParseAsFromFile() { + def file = File.createTempFile('test', '.yml') + file.deleteOnExit() + file.text = 'host: localhost\nport: 9090' + def config = new YamlSlurper().parseAs(ServerConfig, file) + assert config.port == 9090 + } + + void testParseAsFromPath() { + def file = File.createTempFile('test', '.yml') + file.deleteOnExit() + file.text = 'host: example.com\nport: 443' + def config = new YamlSlurper().parseAs(ServerConfig, file.toPath()) + assert config.host == 'example.com' + } + + void testParseAsFromInputStream() { + def stream = new ByteArrayInputStream('host: localhost\nport: 3000'.bytes) + def config = new YamlSlurper().parseAs(ServerConfig, stream) + assert config.port == 3000 + } + void testParseMultiDocs() { def ys = new YamlSlurper() def yaml = ys.parseText '''\
