JsonMapObjectReaderWriterTest.java

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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
 *
 * http://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.apache.cxf.jaxrs.json.basic;

import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.cxf.helpers.CastUtils;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class JsonMapObjectReaderWriterTest {

    @Test
    public void testWriteMap() throws Exception {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("a", "aValue");
        map.put("b", 123);
        map.put("c", Collections.singletonList("cValue"));
        map.put("claim", null);
        String json = new JsonMapObjectReaderWriter().toJson(map);
        assertEquals("{\"a\":\"aValue\",\"b\":123,\"c\":[\"cValue\"],\"claim\":null}",
                     json);
    }
    @Test
    public void testWriteDateProperty() throws Exception {
        Date date = new Date();
        Map<String, Object> map = Collections.singletonMap("createdAt", date);
        String json = new JsonMapObjectReaderWriter().toJson(map);
        assertEquals("{\"createdAt\":\"" + date.toString() + "\"}", json);
    }
    @Test
    public void testReadMap() throws Exception {
        String json = "{\"a\":\"aValue\",\"b\":123,\"c\":[\"cValue\"],\"f\":null}";
        Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json);
        assertEquals(4, map.size());
        assertEquals("aValue", map.get("a"));
        assertEquals(123L, map.get("b"));
        assertEquals(Collections.singletonList("cValue"), map.get("c"));
        assertNull(map.get("f"));
    }
    @Test
    public void testReadMapWithValueCommas() throws Exception {
        String json = "{\"a\":\"aValue1,aValue2\",\"b\":\"bValue1\"\r\n,\"c\":[\"cValue1, cValue2\"],"
            + "\"d\":\"dValue1,dValue2,dValue3,dValue4\"}";
        Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json);
        assertEquals(4, map.size());
        assertEquals("aValue1,aValue2", map.get("a"));
        assertEquals("bValue1", map.get("b"));
        assertEquals(Collections.singletonList("cValue1, cValue2"), map.get("c"));
        assertEquals("dValue1,dValue2,dValue3,dValue4", map.get("d"));
    }
    @Test
    public void testReadStringWithLeftCurlyBracketInString() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        String s = "{\"x\":{\"y\":\"{\"}}";
        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(s);
        assertEquals(1, map.size());
        Map<String, Object> xMap = CastUtils.cast((Map<?, ?>)map.get("x"));
        assertEquals(1, xMap.size());
        assertEquals("{", xMap.get("y"));
    }
    @Test
    public void testReadStringWithLeftCurlyBracketInString2() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        String s = "{\"x\":{\"y\":\"{\", \"z\":\"{\"}, \"a\":\"b\"}";
        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(s);
        assertEquals(2, map.size());
        assertEquals("b", map.get("a"));
        Map<String, Object> xMap = CastUtils.cast((Map<?, ?>)map.get("x"));
        assertEquals(2, xMap.size());
        assertEquals("{", xMap.get("y"));
        assertEquals("{", xMap.get("z"));
    }
    @Test
    public void testReadStringWithRightCurlyBracketInString() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        String s = "{\"x\":{\"y\":\"}\"}}";
        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(s);
        assertEquals(1, map.size());
        Map<String, Object> xMap = CastUtils.cast((Map<?, ?>)map.get("x"));
        assertEquals(1, xMap.size());
        assertEquals("}", xMap.get("y"));
    }
    @Test
    public void testReadStringWithRightCurlyBracketInString2() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        String s = "{\"x\":{\"y\":\"}\", \"z\":\"}\"}, \"a\":\"b\"}";
        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(s);
        assertEquals(2, map.size());
        assertEquals("b", map.get("a"));
        Map<String, Object> xMap = CastUtils.cast((Map<?, ?>)map.get("x"));
        assertEquals(2, xMap.size());
        assertEquals("}", xMap.get("y"));
        assertEquals("}", xMap.get("z"));
    }
    @Test
    public void testReadStringWithCurlyBracketsInString() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        String s = "{\"x\":{\"y\":\"{\\\"}\"}}";
        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(s);
        assertEquals(1, map.size());
        Map<String, Object> xMap = CastUtils.cast((Map<?, ?>)map.get("x"));
        assertEquals(1, xMap.size());
        assertEquals("{\"}", xMap.get("y"));
    }

    @Test
    public void testEscapedForwardSlashInString() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        String s = "{\"kid\":\"4pZbe4shQQGzZXHbeIlbDvmHOc1\\/H6jH6oBk3nUrcZE=\",\"alg\":\"RS256\"}";

        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(s);
        assertEquals(2, map.size());

        String kid = (String)map.get("kid");
        String expectedKid = "4pZbe4shQQGzZXHbeIlbDvmHOc1/H6jH6oBk3nUrcZE=";
        assertEquals(expectedKid, kid);
    }

    @Test(expected = UncheckedIOException.class)
    public void testMalformedInput() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        String s = "{\"nonce\":\"\",:V\"'";
        jsonMapObjectReaderWriter.fromJson(s);
    }

    @Test
    public void testEscapeDoubleQuotes() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        Map<String, Object> content = new HashMap<>();
        content.put("userInput", "a\"");
        String json = jsonMapObjectReaderWriter.toJson(content);
        assertTrue(json.contains("a\\\""));

        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(json);
        assertEquals(1, map.size());
        Map.Entry<String, Object> entry = map.entrySet().iterator().next();
        assertEquals("userInput", entry.getKey());
        assertEquals("a\"", entry.getValue());
    }

    @Test
    public void testAlreadyEscapedDoubleQuotes() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        Map<String, Object> content = new HashMap<>();
        content.put("userInput", "a\\\"");
        String json = jsonMapObjectReaderWriter.toJson(content);
        assertTrue(json.contains("a\\\""));

        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(json);
        assertEquals(1, map.size());
        Map.Entry<String, Object> entry = map.entrySet().iterator().next();
        assertEquals("userInput", entry.getKey());
        assertEquals("a\"", entry.getValue());
    }

    @Test
    public void testEscapeBackslash() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        Map<String, Object> content = new HashMap<>();
        content.put("userInput", "a\\");
        String json = jsonMapObjectReaderWriter.toJson(content);
        assertTrue(json.contains("a\\\\"));

        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(json);
        assertEquals(1, map.size());
        Map.Entry<String, Object> entry = map.entrySet().iterator().next();
        assertEquals("userInput", entry.getKey());
        assertEquals("a\\", entry.getValue());
    }

    /**
     * Regression test for a bug in {@code getNextSepCharIndex}: the method only checks whether
     * the single character immediately before a {@code "} is a backslash when deciding whether
     * the quote is escaped.  That single-character look-back is wrong when a string ends with
     * {@code \\} (an escaped backslash): the second {@code \} is mistaken for an escape prefix
     * of the closing {@code "}, so the parser never exits "in-string" mode, swallows the
     * subsequent comma, and absorbs the rest of the JSON (including any following keys) into
     * the value of the preceding key.
     *
     * <p>Correct behaviour: {@code "\\"} in JSON is a string whose value is a single backslash
     * {@code \}.  The {@code "} that closes it must <em>not</em> be treated as escaped.
     */
    @Test
    public void testReadStringValueEndingWithEscapedBackslashNotLastKey() throws Exception {
        // JSON: {"a":"\\","b":"w"}
        // "a" has value \ (single backslash); "b" has value w.
        // Bug: getNextSepCharIndex sees \ before the closing " of "\\" and skips
        // that quote, causing "b" to be swallowed into the value of "a".
        String json = "{\"a\":\"\\\\\",\"b\":\"w\"}";
        Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json);
        assertEquals(2, map.size());
        assertEquals("\\", map.get("a"));
        assertEquals("w", map.get("b"));
    }

    /**
     * Same bug as {@link #testReadStringValueEndingWithEscapedBackslashNotLastKey} but with a
     * security-relevant follow-on key, matching the attack scenario described in the audit:
     * a crafted value ending in {@code \\} causes a subsequent key such as {@code "admin"} to
     * disappear from the parsed map.
     */
    @Test
    public void testReadStringValueEndingWithEscapedBackslashDropsSubsequentKey() throws Exception {
        // JSON: {"role":"user\\","admin":true}
        // "role" value is user\ (user + single backslash); "admin" value is Boolean.TRUE.
        // Bug: "admin" key is consumed as part of the "role" value and absent from the result.
        String json = "{\"role\":\"user\\\\\",\"admin\":true}";
        Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json);
        assertEquals(2, map.size());
        assertEquals("user\\", map.get("role"));
        assertEquals(Boolean.TRUE, map.get("admin"));
    }

    /**
     * Regression test for "Key Names With Escaped Quotes Parsed Incorrectly".
     *
     * <p>{@code readJsonObjectAsSettable} extracts a key name with a plain
     * {@code json.indexOf(DQUOTE, i + 1)}, which stops at the first {@code "} it finds
     * regardless of whether that quote is escaped.  A key that contains an embedded
     * escaped quote ��� e.g. {@code "foo\"bar"} ��� is therefore truncated: the method
     * finds the {@code "} in {@code \"} and returns {@code foo\} instead of
     * {@code foo"bar}.
     *
     * <p>The parser then searches for the value separator {@code :} starting from the
     * wrong offset, so the remainder of the key ({@code bar}) and the colon are
     * consumed as a suffix of the (wrong) key name.  The resulting map contains an
     * entry with the wrong key and the test assertion on {@code map.get("foo\"bar")}
     * returns {@code null}.
     */
    @Test
    public void testKeyWithEscapedQuoteIsParsedCorrectly() throws Exception {
        // JSON: {"foo\"bar":"value"}  ��� key contains an embedded double-quote character
        // Bug: indexOf('"') stops at the \" inside the key, producing truncated key "foo\"
        // instead of the correct key foo"bar.
        String json = "{\"foo\\\"bar\":\"value\"}";
        Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json);
        assertEquals(1, map.size());
        assertEquals("value", map.get("foo\"bar"));
    }

    @Test
    public void testAlreadyEscapedBackslash() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        Map<String, Object> content = new HashMap<>();
        content.put("userInput", "a\\\\");
        String json = jsonMapObjectReaderWriter.toJson(content);
        assertTrue(json.contains("a\\\\"));

        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(json);
        assertEquals(1, map.size());
        Map.Entry<String, Object> entry = map.entrySet().iterator().next();
        assertEquals("userInput", entry.getKey());
        assertEquals("a\\", entry.getValue());
    }

    /**
     * Writer-side counterpart of {@link #testReadStringValueEndingWithEscapedBackslashDropsSubsequentKey}.
     * {@code escapeJson} only looked at the single character before a {@code "}/{@code \} to decide
     * whether it was already escaped, so a value ending in an escaped backslash pair ({@code \\})
     * followed by content left the next quote raw, breaking out of the JSON string.
     */
    @Test
    public void testEscapeQuoteAfterEscapedBackslash() throws Exception {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        Map<String, Object> content = new LinkedHashMap<>();
        // value is: \ (escaped backslash) followed by a raw quote and an injected key
        content.put("role", "user\\\\\",\"admin\":true");
        String json = jsonMapObjectReaderWriter.toJson(content);

        Map<String, Object> map = jsonMapObjectReaderWriter.fromJson(json);
        assertEquals(1, map.size());
        assertEquals("user\\\",\"admin\":true", map.get("role"));
        assertNull(map.get("admin"));
    }

    /**
     * Regression test for "[MEDIUM] Unicode Escapes Not Decoded ��� Potential Bypass".
     *
     * <p>RFC 8259 section 7 requires that four-digit hex Unicode escape sequences
     * (backslash + u + four hex digits) be decoded to the corresponding character.
     * {@code readPrimitiveValue} only handles {@code \/}, {@code \"}, and {@code \\};
     * four-digit hex escapes and single-character escapes ({@code \n}, {@code \r},
     * {@code \t}, etc.) are returned as the raw literal escape text rather than the
     * decoded character.
     *
     * <p>Security impact: a JWT whose {@code alg} header is written using four-digit hex
     * escapes that spell {@code none} passes CXF's own algorithm check (the literal
     * un-decoded sequence is not equal to {@code "none"}), while a downstream
     * RFC-compliant consumer decodes the escapes and may skip signature verification
     * entirely ��� a parser-differential bypass.
     */
    @Test
    public void testUnicodeEscapeInValueDecodedCorrectly() throws Exception {
        // JSON: {"alg":"<none-as-4-digit-hex-escapes>"} ��� each character of "none" is written
        // as its four-digit hex Unicode escape.  A correct parser decodes them to "none".
        // Bug: readPrimitiveValue does not decode four-digit hex escapes; the value is
        // returned as the 24-character literal sequence rather than "none".
        String json = "{\"alg\":\"\\u006e\\u006f\\u006e\\u0065\"}";
        Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json);
        assertEquals(1, map.size());
        assertEquals("none", map.get("alg"));
    }

    /**
     * Simpler companion to {@link #testUnicodeEscapeInValueDecodedCorrectly}: verifies
     * that a four-digit hex Unicode escape embedded in the middle of a value string is
     * decoded to the target character rather than kept as the raw escape text.
     *
     * <p>The letter {@code l} is U+006C; JSON {@code "hello"} should therefore
     * produce the five-character string {@code hello}.
     */
    @Test
    public void testUnicodeEscapeEmbeddedInString() throws Exception {
        // JSON: {"a":"hel<U+006C>o"} ��� U+006C is 'l', so the decoded value is "hello".
        // Bug: the six-character literal sequence is returned instead of the decoded char.
        String json = "{\"a\":\"hel\\u006co\"}";
        Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json);
        assertEquals(1, map.size());
        assertEquals("hello", map.get("a"));
    }

    /**
     * RFC 8259 section 7 requires that all control characters (U+0000���U+001F) in string
     * values be escaped in JSON output.  {@code escapeJson} only escapes {@code "} and
     * {@code \}; every other control character is emitted verbatim, producing JSON that
     * violates the specification and may be rejected or mishandled by strict parsers.
     *
     * <p>The three tests below cover the most security-relevant cases:
     * <ol>
     *   <li>A raw line-feed (U+000A) must be escaped as {@code \n}.</li>
     *   <li>A raw horizontal-tab (U+0009) must be escaped as {@code \t}.</li>
     *   <li>A raw CR+LF sequence must have both bytes escaped ��� an unescaped CR+LF in a
     *       JSON value that is subsequently placed in an HTTP response header enables
     *       HTTP response splitting (header injection).</li>
     * </ol>
     */
    @Test
    public void testRawNewlineInValueIsEscapedInOutput() throws Exception {
        // Bug: escapeJson passes U+000A through verbatim; correct output is \n (two chars).
        Map<String, Object> map = Collections.singletonMap("msg", "line1\nline2");
        String json = new JsonMapObjectReaderWriter().toJson(map);
        assertFalse("Raw newline must not appear verbatim in JSON output", json.contains("\n"));
        assertEquals("{\"msg\":\"line1\\nline2\"}", json);
    }

    @Test
    public void testRawTabInValueIsEscapedInOutput() throws Exception {
        // Bug: escapeJson passes U+0009 through verbatim; correct output is \t (two chars).
        Map<String, Object> map = Collections.singletonMap("msg", "col1\tcol2");
        String json = new JsonMapObjectReaderWriter().toJson(map);
        assertFalse("Raw tab must not appear verbatim in JSON output", json.contains("\t"));
        assertEquals("{\"msg\":\"col1\\tcol2\"}", json);
    }

    @Test
    public void testCrLfInValueDoesNotEnableHttpResponseSplitting() throws Exception {
        // Bug: neither \r nor \n is escaped, so a crafted value can inject arbitrary
        // HTTP headers when the JSON output is placed in a response header field.
        Map<String, Object> map = Collections.singletonMap("v", "ok\r\nX-Injected: evil");
        String json = new JsonMapObjectReaderWriter().toJson(map);
        assertFalse("Raw CR must not appear verbatim in JSON output", json.contains("\r"));
        assertFalse("Raw LF must not appear verbatim in JSON output", json.contains("\n"));
    }

    @Test
    public void testRejectInfinityNumericValue() {
        assertInvalidNumericLiteral("Infinity");
        assertInvalidNumericLiteral("-Infinity");
    }

    @Test
    public void testRejectNaNNumericValue() {
        assertInvalidNumericLiteral("NaN");
    }

    @Test
    public void testNestedArrayValueParsesSuccessfully() {
        Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson("{\"a\":[[]]}");
        assertEquals(1, map.size());
        assertEquals(Collections.singletonList(Collections.emptyList()), map.get("a"));
    }

    private void assertInvalidNumericLiteral(String value) {
        JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
        try {
            jsonMapObjectReaderWriter.fromJson("{\"exp\":" + value + "}");
            fail("Expected NumberFormatException for invalid numeric value: " + value);
        } catch (NumberFormatException ex) {
            // expected
        }
    }

    /**
     * Add a test to check an exception is thrown on parsing deeply nested JSON structures that exceed the 
     * recursion depth limit.
     */
    @Test(expected = UncheckedIOException.class)
    public void testDepthLimitExceededThrowsUncheckedIOException() {
        int levels = JsonMapObjectReaderWriter.MAX_RECURSION_DEPTH + 2;
        StringBuilder sb = new StringBuilder(levels * 8);
        for (int i = 0; i < levels; i++) {
            sb.append("{\"a\":");
        }
        sb.append("\"v\"");
        for (int i = 0; i < levels; i++) {
            sb.append('}');
        }
        new JsonMapObjectReaderWriter().fromJson(sb.toString());
    }

    /**
     * A payload with exactly {@code MAX_RECURSION_DEPTH + 1} brace levels reaches a
     * maximum internal depth of {@code MAX_RECURSION_DEPTH} ��� right at the boundary ���
     * and must parse successfully.
     */
    @Test
    public void testDepthLimitNotExceededParsesSuccessfully() {
        int levels = JsonMapObjectReaderWriter.MAX_RECURSION_DEPTH + 1;
        StringBuilder sb = new StringBuilder(levels * 8);
        for (int i = 0; i < levels; i++) {
            sb.append("{\"a\":");
        }
        sb.append("\"v\"");
        for (int i = 0; i < levels; i++) {
            sb.append('}');
        }
        // Should not throw
        new JsonMapObjectReaderWriter().fromJson(sb.toString());
    }

    @Test(expected = UncheckedIOException.class)
    public void testWriterPathDeeplyNestedMapThrowsUncheckedIOException() {
        new JsonMapObjectReaderWriter().toJson(createNestedMap(20000));
    }

    @Test(expected = UncheckedIOException.class)
    public void testObjectKeyLimitExceededThrowsUncheckedIOException() {
        StringBuilder sb = new StringBuilder(JsonMapObjectReaderWriter.DEFAULT_MAX_OBJECT_KEYS * 10);
        sb.append('{');
        for (int i = 0; i <= JsonMapObjectReaderWriter.DEFAULT_MAX_OBJECT_KEYS; i++) {
            if (i > 0) {
                sb.append(',');
            }
            sb.append('"').append('k').append(i).append('"').append(':').append('1');
        }
        sb.append('}');

        new JsonMapObjectReaderWriter().fromJson(sb.toString());
    }

    @Test(expected = UncheckedIOException.class)
    public void testArrayElementLimitExceededThrowsUncheckedIOException() {
        StringBuilder sb = new StringBuilder(JsonMapObjectReaderWriter.DEFAULT_MAX_ARRAY_ELEMENTS * 3);
        sb.append('{').append('"').append('a').append('"').append(':').append('[');
        for (int i = 0; i <= JsonMapObjectReaderWriter.DEFAULT_MAX_ARRAY_ELEMENTS; i++) {
            if (i > 0) {
                sb.append(',');
            }
            sb.append('1');
        }
        sb.append(']').append('}');

        new JsonMapObjectReaderWriter().fromJson(sb.toString());
    }

    @Test
    public void testConfiguredObjectAndArrayLimits() {
        System.setProperty(JsonMapObjectReaderWriter.MAX_OBJECT_KEYS_PROPERTY, "2");
        System.setProperty(JsonMapObjectReaderWriter.MAX_ARRAY_ELEMENTS_PROPERTY, "2");
        try {
            JsonMapObjectReaderWriter rw = new JsonMapObjectReaderWriter();
            rw.fromJson("{\"a\":1,\"b\":2}");
            rw.fromJson("{\"a\":[1,2]}");

            try {
                rw.fromJson("{\"a\":1,\"b\":2,\"c\":3}");
                fail("Expected object key count limit exception");
            } catch (UncheckedIOException ex) {
                // expected
            }

            try {
                rw.fromJson("{\"a\":[1,2,3]}");
                fail("Expected array element count limit exception");
            } catch (UncheckedIOException ex) {
                // expected
            }
        } finally {
            System.clearProperty(JsonMapObjectReaderWriter.MAX_OBJECT_KEYS_PROPERTY);
            System.clearProperty(JsonMapObjectReaderWriter.MAX_ARRAY_ELEMENTS_PROPERTY);
        }
    }

    private Map<String, Object> createNestedMap(int depth) {
        Map<String, Object> root = new HashMap<>();
        Map<String, Object> current = root;
        for (int i = 0; i < depth; i++) {
            Map<String, Object> child = new HashMap<>();
            current.put("k", child);
            current = child;
        }
        return root;
    }

}