From 0c9f40b171bfca68ddd1622455aefd34e2e4bb92 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 22 Jan 2026 22:11:17 -0800 Subject: [PATCH] Allow YAML processors to create a flattened map with nulls included Add an additional `getFlattenedMap` method to `YamlProcessor` to allow the resulting flattened map to include nulls. This update will allow processor subclasses to tell the difference between YAML that is defined with an empty object vs missing the key entirely: e.g.: application: name: test optional: {} vs application: name: test optional: {} Closes gh-36197 Signed-off-by: Phillip Webb --- .../beans/factory/config/YamlProcessor.java | 27 +++++++++++++++--- .../factory/config/YamlProcessorTests.java | 28 +++++++++++++++++-- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java index 13648d824aa..fa393d32b36 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -304,13 +304,32 @@ public abstract class YamlProcessor { * @since 4.1.3 */ protected final Map getFlattenedMap(Map source) { + return getFlattenedMap(source, false); + } + + /** + * Return a flattened version of the given map, recursively following any nested Map + * or Collection values. Entries from the resulting map retain the same order as the + * source. When called with the Map from a {@link MatchCallback} the result will + * contain the same values as the {@link MatchCallback} Properties. + * @param source the source map + * @param includeNulls if {@code null} entries can be included in the result + * @return a flattened map + * @since 7.0.4 + */ + protected final Map getFlattenedMap(Map source, boolean includeNulls) { Map result = new LinkedHashMap<>(); - buildFlattenedMap(result, source, null); + buildFlattenedMap(result, source, null, includeNulls); return result; } @SuppressWarnings({"rawtypes", "unchecked"}) - private void buildFlattenedMap(Map result, Map source, @Nullable String path) { + private void buildFlattenedMap(Map result, Map source, @Nullable String path, + boolean includeNulls) { + if (includeNulls && source.isEmpty()) { + result.put(path, null); + return; + } source.forEach((key, value) -> { if (StringUtils.hasText(path)) { if (key.startsWith("[")) { @@ -325,7 +344,7 @@ public abstract class YamlProcessor { } else if (value instanceof Map map) { // Need a compound key - buildFlattenedMap(result, map, key); + buildFlattenedMap(result, map, key, includeNulls); } else if (value instanceof Collection collection) { // Need a compound key @@ -336,7 +355,7 @@ public abstract class YamlProcessor { int count = 0; for (Object object : collection) { buildFlattenedMap(result, Collections.singletonMap( - "[" + (count++) + "]", object), key); + "[" + (count++) + "]", object), key, includeNulls); } } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java index b0cf6fa7be7..a134b6bf697 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java @@ -44,8 +44,7 @@ import static org.assertj.core.api.InstanceOfAssertFactories.set; */ class YamlProcessorTests { - private final YamlProcessor processor = new YamlProcessor() { - }; + private final TestYamlProcessor processor = new TestYamlProcessor(); @Test @@ -182,8 +181,33 @@ class YamlProcessorTests { .withMessageContaining("Global tag is not allowed: tag:yaml.org,2002:java.net.URL"); } + @Test + void processAndFlattenWithoutIncludedNulls() { + setYaml("foo: bar\nbar:\n spam: {}"); + Map flattened = this.processor.processAndFlatten(false); + assertThat(flattened).containsEntry("foo", "bar").doesNotContainKey("bar.spam").hasSize(1); + } + + @Test + void processAndFlattenWithIncludedNulls() { + setYaml("foo: bar\nbar:\n spam: {}"); + Map flattened = this.processor.processAndFlatten(true); + assertThat(flattened).containsEntry("foo", "bar").containsEntry("bar.spam", null).hasSize(2); + } + private void setYaml(String yaml) { this.processor.setResources(new ByteArrayResource(yaml.getBytes())); } + private static class TestYamlProcessor extends YamlProcessor { + + Map processAndFlatten(boolean includeNulls) { + Map flattened = new LinkedHashMap<>(); + process((properties, map) -> flattened.putAll(getFlattenedMap(map, includeNulls))); + return flattened; + } + + + } + }