From ab5d53d1eb465354be81ca679f263f7695767e11 Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Sun, 13 Sep 2026 07:44:03 +0700 Subject: [PATCH] Restore empty YAML arrays in JSON and YAML responses Flattened config properties encode empty collections as an empty string. When rebuilding nested JSON/YAML output, restore those markers to empty lists so values like network.urls: [] round-trip correctly. Fixes gh-2746 Signed-off-by: arimu1 <19286898+arimu1@users.noreply.github.com> --- .../environment/EnvironmentController.java | 24 +++++++++++++++++++ .../EnvironmentControllerTests.java | 20 ++++++++++++++++ .../NativeEnvironmentRepositoryTests.java | 23 ++++++++++++++++++ .../test/empty-array/application.yml | 2 ++ 4 files changed, 69 insertions(+) create mode 100644 spring-cloud-config-server/src/test/resources/test/empty-array/application.yml diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java index 7940a31e10..266c2abd46 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java @@ -20,10 +20,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; @@ -340,6 +342,28 @@ private Map convertToProperties(Environment profiles) { private void postProcessProperties(Map propertiesMap) { propertiesMap.keySet().removeIf(key -> key.equals("spring.profiles")); + restoreEmptyCollections(propertiesMap); + } + + /** + * Flattened YAML properties represent empty collections as an empty string (see + * {@code YamlProcessor#buildFlattenedMap}). When rebuilding nested YAML/JSON, restore + * those markers to empty lists unless indexed entries exist for the same prefix. + */ + private void restoreEmptyCollections(Map propertiesMap) { + Set arrayPrefixes = new HashSet<>(); + for (String key : propertiesMap.keySet()) { + int indexOfBracket = key.indexOf('['); + if (indexOfBracket > 0) { + arrayPrefixes.add(key.substring(0, indexOfBracket)); + } + } + for (Entry entry : propertiesMap.entrySet()) { + String key = entry.getKey(); + if (!key.contains("[") && "".equals(entry.getValue()) && !arrayPrefixes.contains(key)) { + entry.setValue(Collections.emptyList()); + } + } } /** diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java index bd47633f09..a7999f6333 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java @@ -183,6 +183,26 @@ public void arrayInYaml() throws Exception { assertThat(yaml).isEqualTo("a:\n b:\n - c\n - d\n"); } + @Test + public void emptyArrayInJson() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("network.urls", ""); + this.environment.add(new PropertySource("one", map)); + when(this.repository.findOne("foo", "bar", null, false)).thenReturn(this.environment); + String json = this.controller.jsonProperties("foo", "bar", false).getBody(); + JSONAssert.assertEquals("{\"network\":{\"urls\":[]}}", json, JSONCompareMode.STRICT); + } + + @Test + public void emptyArrayInYaml() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("network.urls", ""); + this.environment.add(new PropertySource("one", map)); + when(this.repository.findOne("foo", "bar", null, false)).thenReturn(this.environment); + String yaml = this.controller.yaml("foo", "bar", false).getBody(); + assertThat(yaml).isEqualTo("network:\n urls: []\n"); + } + @Test public void yamlWithBrackets() throws Exception { Map map = new LinkedHashMap(); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java index 57d024cefe..10a8018927 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepositoryTests.java @@ -23,6 +23,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; @@ -30,6 +32,7 @@ import org.springframework.cloud.config.environment.PropertySource; import org.springframework.cloud.config.server.environment.SearchPathLocator.Locations; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.ResponseEntity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -112,6 +115,26 @@ public void prefixedMultiDocProperties() { .isNotEqualTo(environment.getPropertySources().get(1).getName()); } + @Test + public void emptyYamlArrayInJsonOutput() throws Exception { + this.repository.setSearchLocations("classpath:/test/empty-array"); + Environment environment = this.repository.findOne("application", "default", "master"); + EnvironmentRepository repository = new EnvironmentRepository() { + @Override + public Environment findOne(String application, String profile, String label) { + return environment; + } + + @Override + public Environment findOne(String application, String profile, String label, boolean includeOrigin) { + return environment; + } + }; + EnvironmentController controller = new EnvironmentController(repository); + ResponseEntity json = controller.labelledJsonProperties("application", "default", "master", false); + JSONAssert.assertEquals("{\"network\":{\"urls\":[]}}", json.getBody(), JSONCompareMode.STRICT); + } + @Test public void prefixedWithFile() { this.repository.setSearchLocations("file:./src/test/resources/test"); diff --git a/spring-cloud-config-server/src/test/resources/test/empty-array/application.yml b/spring-cloud-config-server/src/test/resources/test/empty-array/application.yml new file mode 100644 index 0000000000..50882e6205 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/test/empty-array/application.yml @@ -0,0 +1,2 @@ +network: + urls: []