diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/BasicJsonWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/BasicJsonWriter.java
new file mode 100644
index 00000000000..4fa022252ae
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/aot/nativex/BasicJsonWriter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Very basic json writer for the purposes of translating runtime hints to native
+ * configuration.
+ *
+ * @author Stephane Nicoll
+ */
+class BasicJsonWriter {
+
+ private final IndentingWriter writer;
+
+ /**
+ * Create a new instance with the specified indent value.
+ * @param writer the writer to use
+ * @param singleIndent the value of one indent
+ */
+ public BasicJsonWriter(Writer writer, String singleIndent) {
+ this.writer = new IndentingWriter(writer, singleIndent);
+ }
+
+ /**
+ * Create a new instance using two whitespaces for the indent.
+ * @param writer the writer to use
+ */
+ public BasicJsonWriter(Writer writer) {
+ this(writer, " ");
+ }
+
+
+ /**
+ * Write an object with the specified attributes. Each attribute is
+ * written according to its value type:
+ *
+ * - Map: write the value as a nested object
+ * - List: write the value as a nested array
+ * - Otherwise, write a single value
+ *
+ * @param attributes the attributes of the object
+ */
+ public void writeObject(Map attributes) {
+ writeObject(attributes, true);
+ }
+
+ /**
+ * Write an array with the specified items. Each item in the
+ * list is written either as a nested object or as an attribute
+ * depending on its type.
+ * @param items the items to write
+ * @see #writeObject(Map)
+ */
+ public void writeArray(List> items) {
+ writeArray(items, true);
+ }
+
+ private void writeObject(Map attributes, boolean newLine) {
+ if (attributes.isEmpty()) {
+ this.writer.print("{ }");
+ }
+ else {
+ this.writer.println("{").indented(writeAll(attributes.entrySet().iterator(),
+ entry -> writeAttribute(entry.getKey(), entry.getValue()))).print("}");
+ }
+ if (newLine) {
+ this.writer.println();
+ }
+ }
+
+ private void writeArray(List> items, boolean newLine) {
+ if (items.isEmpty()) {
+ this.writer.print("[ ]");
+ }
+ else {
+ this.writer.println("[")
+ .indented(writeAll(items.iterator(), this::writeValue)).print("]");
+ }
+ if (newLine) {
+ this.writer.println();
+ }
+ }
+
+ private Runnable writeAll(Iterator it, Consumer writer) {
+ return () -> {
+ while (it.hasNext()) {
+ writer.accept(it.next());
+ if (it.hasNext()) {
+ this.writer.println(",");
+ }
+ else {
+ this.writer.println();
+ }
+ }
+ };
+ }
+
+ private void writeAttribute(String name, Object value) {
+ this.writer.print(quote(name) + ": ");
+ writeValue(value);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void writeValue(Object value) {
+ if (value instanceof Map, ?> map) {
+ writeObject((Map) map, false);
+ }
+ else if (value instanceof List> list) {
+ writeArray(list, false);
+ }
+ else if (value instanceof CharSequence string) {
+ this.writer.print(quote(escape(string)));
+ }
+ else if (value instanceof Boolean flag) {
+ this.writer.print(Boolean.toString(flag));
+ }
+ else {
+ throw new IllegalStateException("unsupported type: " + value.getClass());
+ }
+ }
+
+ private String quote(String name) {
+ return "\"" + name + "\"";
+ }
+
+ private static String escape(CharSequence input) {
+ StringBuilder builder = new StringBuilder();
+ input.chars().forEach(c -> {
+ switch (c) {
+ case '"':
+ builder.append("\\\"");
+ break;
+ case '\\':
+ builder.append("\\\\");
+ break;
+ case '/':
+ builder.append("\\/");
+ break;
+ case '\b':
+ builder.append("\\b");
+ break;
+ case '\f':
+ builder.append("\\f");
+ break;
+ case '\n':
+ builder.append("\\n");
+ break;
+ case '\r':
+ builder.append("\\r");
+ break;
+ case '\t':
+ builder.append("\\t");
+ break;
+ default:
+ if (c <= 0x1F) {
+ builder.append(String.format("\\u%04x", c));
+ }
+ else {
+ builder.append((char) c);
+ }
+ break;
+ }
+ });
+ return builder.toString();
+ }
+
+
+ static class IndentingWriter extends Writer {
+
+ private final Writer out;
+
+ private final String singleIndent;
+
+ private int level = 0;
+
+ private String currentIndent = "";
+
+ private boolean prependIndent = false;
+
+ IndentingWriter(Writer out, String singleIndent) {
+ this.out = out;
+ this.singleIndent = singleIndent;
+ }
+
+ /**
+ * Write the specified text.
+ * @param string the content to write
+ */
+ public IndentingWriter print(String string) {
+ write(string.toCharArray(), 0, string.length());
+ return this;
+ }
+
+ /**
+ * Write the specified text and append a new line.
+ * @param string the content to write
+ */
+ public IndentingWriter println(String string) {
+ write(string.toCharArray(), 0, string.length());
+ return println();
+ }
+
+ /**
+ * Write a new line.
+ */
+ public IndentingWriter println() {
+ String separator = System.lineSeparator();
+ try {
+ this.out.write(separator.toCharArray(), 0, separator.length());
+ }
+ catch (IOException ex) {
+ throw new IllegalStateException(ex);
+ }
+ this.prependIndent = true;
+ return this;
+ }
+
+ /**
+ * Increase the indentation level and execute the {@link Runnable}. Decrease the
+ * indentation level on completion.
+ * @param runnable the code to execute withing an extra indentation level
+ */
+ public IndentingWriter indented(Runnable runnable) {
+ indent();
+ runnable.run();
+ return outdent();
+ }
+
+ /**
+ * Increase the indentation level.
+ */
+ private IndentingWriter indent() {
+ this.level++;
+ return refreshIndent();
+ }
+
+ /**
+ * Decrease the indentation level.
+ */
+ private IndentingWriter outdent() {
+ this.level--;
+ return refreshIndent();
+ }
+
+ private IndentingWriter refreshIndent() {
+ this.currentIndent = this.singleIndent.repeat(Math.max(0, this.level));
+ return this;
+ }
+
+ @Override
+ public void write(char[] chars, int offset, int length) {
+ try {
+ if (this.prependIndent) {
+ this.out.write(this.currentIndent.toCharArray(), 0, this.currentIndent.length());
+ this.prependIndent = false;
+ }
+ this.out.write(chars, offset, length);
+ }
+ catch (IOException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ this.out.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ this.out.close();
+ }
+
+ }
+
+}
diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/JsonUtils.java b/spring-core/src/main/java/org/springframework/aot/nativex/JsonUtils.java
deleted file mode 100644
index 15ed1b58e6d..00000000000
--- a/spring-core/src/main/java/org/springframework/aot/nativex/JsonUtils.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2002-2022 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.aot.nativex;
-
-/**
- * Utility class for JSON.
- *
- * @author Sebastien Deleuze
- */
-abstract class JsonUtils {
-
- /**
- * Escape a JSON String.
- */
- static String escape(String input) {
- StringBuilder builder = new StringBuilder();
- input.chars().forEach(c -> {
- switch (c) {
- case '"':
- builder.append("\\\"");
- break;
- case '\\':
- builder.append("\\\\");
- break;
- case '/':
- builder.append("\\/");
- break;
- case '\b':
- builder.append("\\b");
- break;
- case '\f':
- builder.append("\\f");
- break;
- case '\n':
- builder.append("\\n");
- break;
- case '\r':
- builder.append("\\r");
- break;
- case '\t':
- builder.append("\\t");
- break;
- default:
- if (c <= 0x1F) {
- builder.append(String.format("\\u%04x", c));
- }
- else {
- builder.append((char) c);
- }
- break;
- }
- });
- return builder.toString();
- }
-}
diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/BasicJsonWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/BasicJsonWriterTests.java
new file mode 100644
index 00000000000..4545c9caa9c
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/aot/nativex/BasicJsonWriterTests.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.aot.nativex;
+
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link BasicJsonWriter}.
+ *
+ * @author Stephane Nicoll
+ */
+class BasicJsonWriterTests {
+
+ private final StringWriter out = new StringWriter();
+
+ private final BasicJsonWriter json = new BasicJsonWriter(out, "\t");
+
+ @Test
+ void writeObject() {
+ Map attributes = orderedMap("test", "value");
+ attributes.put("another", true);
+ this.json.writeObject(attributes);
+ assertThat(out.toString()).isEqualTo("""
+ {
+ "test": "value",
+ "another": true
+ }
+ """);
+ }
+
+ @Test
+ void writeObjectWitNestedObject() {
+ Map attributes = orderedMap("test", "value");
+ attributes.put("nested", orderedMap("enabled", false));
+ this.json.writeObject(attributes);
+ assertThat(out.toString()).isEqualTo("""
+ {
+ "test": "value",
+ "nested": {
+ "enabled": false
+ }
+ }
+ """);
+ }
+
+ @Test
+ void writeObjectWitNestedArrayOfString() {
+ Map attributes = orderedMap("test", "value");
+ attributes.put("nested", List.of("test", "value", "another"));
+ this.json.writeObject(attributes);
+ assertThat(out.toString()).isEqualTo("""
+ {
+ "test": "value",
+ "nested": [
+ "test",
+ "value",
+ "another"
+ ]
+ }
+ """);
+ }
+
+ @Test
+ void writeObjectWitNestedArrayOfObject() {
+ Map attributes = orderedMap("test", "value");
+ LinkedHashMap secondNested = orderedMap("name", "second");
+ secondNested.put("enabled", false);
+ attributes.put("nested", List.of(orderedMap("name", "first"), secondNested, orderedMap("name", "third")));
+ this.json.writeObject(attributes);
+ assertThat(out.toString()).isEqualTo("""
+ {
+ "test": "value",
+ "nested": [
+ {
+ "name": "first"
+ },
+ {
+ "name": "second",
+ "enabled": false
+ },
+ {
+ "name": "third"
+ }
+ ]
+ }
+ """);
+ }
+
+ @Test
+ void writeObjectWithNestedEmptyArray() {
+ Map attributes = orderedMap("test", "value");
+ attributes.put("nested", Collections.emptyList());
+ this.json.writeObject(attributes);
+ assertThat(out.toString()).isEqualTo("""
+ {
+ "test": "value",
+ "nested": [ ]
+ }
+ """);
+ }
+
+ @Test
+ void writeObjectWithNestedEmptyObject() {
+ Map attributes = orderedMap("test", "value");
+ attributes.put("nested", Collections.emptyMap());
+ this.json.writeObject(attributes);
+ assertThat(out.toString()).isEqualTo("""
+ {
+ "test": "value",
+ "nested": { }
+ }
+ """);
+ }
+
+ @Test
+ void writeWithEscapeDoubleQuote() {
+ assertEscapedValue("foo\"bar", "foo\\\"bar");
+ }
+
+ @Test
+ void writeWithEscapeBackslash() {
+ assertEscapedValue("foo\"bar", "foo\\\"bar");
+ }
+
+ @Test
+ void writeWithEscapeBackspace() {
+ assertEscapedValue("foo\bbar", "foo\\bbar");
+ }
+
+ @Test
+ void writeWithEscapeFormFeed() {
+ assertEscapedValue("foo\fbar", "foo\\fbar");
+ }
+
+ @Test
+ void writeWithEscapeNewline() {
+ assertEscapedValue("foo\nbar", "foo\\nbar");
+ }
+
+ @Test
+ void writeWithEscapeCarriageReturn() {
+ assertEscapedValue("foo\rbar", "foo\\rbar");
+ }
+
+ @Test
+ void writeWithEscapeTab() {
+ assertEscapedValue("foo\tbar", "foo\\tbar");
+ }
+
+ @Test
+ void writeWithEscapeUnicode() {
+ assertEscapedValue("foo\u001Fbar", "foo\\u001fbar");
+ }
+
+ void assertEscapedValue(String value, String expectedEscapedValue) {
+ Map attributes = new LinkedHashMap<>();
+ attributes.put("test", value);
+ this.json.writeObject(attributes);
+ assertThat(out.toString()).contains("\"test\": \"" + expectedEscapedValue + "\"");
+ }
+
+ private static LinkedHashMap orderedMap(String key, Object value) {
+ LinkedHashMap map = new LinkedHashMap<>();
+ map.put(key, value);
+ return map;
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/JsonUtilsTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/JsonUtilsTests.java
deleted file mode 100644
index 1773b8c35c0..00000000000
--- a/spring-core/src/test/java/org/springframework/aot/nativex/JsonUtilsTests.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2002-2022 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.aot.nativex;
-
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests for {@link JsonUtils}.
- *
- * @author Sebastien Deleuze
- */
-public class JsonUtilsTests {
-
- @Test
- void unescaped() {
- assertThat(JsonUtils.escape("azerty")).isEqualTo("azerty");
- }
-
- @Test
- void escapeDoubleQuote() {
- assertThat(JsonUtils.escape("foo\"bar")).isEqualTo("foo\\\"bar");
- }
-
- @Test
- void escapeBackslash() {
- assertThat(JsonUtils.escape("foo\"bar")).isEqualTo("foo\\\"bar");
- }
-
- @Test
- void escapeBackspace() {
- assertThat(JsonUtils.escape("foo\bbar")).isEqualTo("foo\\bbar");
- }
-
- @Test
- void escapeFormfeed() {
- assertThat(JsonUtils.escape("foo\fbar")).isEqualTo("foo\\fbar");
- }
-
- @Test
- void escapeNewline() {
- assertThat(JsonUtils.escape("foo\nbar")).isEqualTo("foo\\nbar");
- }
-
- @Test
- void escapeCarriageReturn() {
- assertThat(JsonUtils.escape("foo\rbar")).isEqualTo("foo\\rbar");
- }
-
- @Test
- void escapeTab() {
- assertThat(JsonUtils.escape("foo\tbar")).isEqualTo("foo\\tbar");
- }
-
- @Test
- void escapeUnicode() {
- assertThat(JsonUtils.escape("foo\u001Fbar")).isEqualTo("foo\\u001fbar");
- }
-}