This is an automated email from the ASF dual-hosted git repository.
wgtmac pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git
The following commit(s) were added to refs/heads/master by this push:
new b9c11afc9 GH-3414: Add parseJson to VariantBuilder for JSON-to-Variant
conversion (#3415)
b9c11afc9 is described below
commit b9c11afc9354af962aa378b2fdd6815d9586b080
Author: gaurav7261 <[email protected]>
AuthorDate: Tue May 12 13:11:43 2026 +0530
GH-3414: Add parseJson to VariantBuilder for JSON-to-Variant conversion
(#3415)
Move parseJson and helpers from VariantBuilder into dedicated
VariantJsonParser
class to isolate Jackson dependency. Add StreamReadConstraints to
JsonFactory
for safety against malicious input. Revert NOTICE (cross-ASF credit not
needed).
Add edge-case tests: empty input, non-JSON, incomplete array, large object.
Ported from Apache Spark's VariantBuilder.parseJson.
---
.gitignore | 1 +
parquet-variant/pom.xml | 11 +
.../apache/parquet/variant/VariantJsonParser.java | 206 +++++++++++++
.../parquet/variant/TestVariantParseJson.java | 343 +++++++++++++++++++++
4 files changed, 561 insertions(+)
diff --git a/.gitignore b/.gitignore
index 2fd06049e..ad049afc9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,4 +20,5 @@ target/
mvn_install.log
.vscode/*
.DS_Store
+.memsearch/
diff --git a/parquet-variant/pom.xml b/parquet-variant/pom.xml
index 31f29d096..26d4d5b53 100644
--- a/parquet-variant/pom.xml
+++ b/parquet-variant/pom.xml
@@ -46,6 +46,17 @@
<artifactId>parquet-column</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>${jackson.groupId}</groupId>
+ <artifactId>jackson-core</artifactId>
+ <version>${jackson.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.parquet</groupId>
+ <artifactId>parquet-jackson</artifactId>
+ <version>${project.version}</version>
+ <scope>runtime</scope>
+ </dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
diff --git
a/parquet-variant/src/main/java/org/apache/parquet/variant/VariantJsonParser.java
b/parquet-variant/src/main/java/org/apache/parquet/variant/VariantJsonParser.java
new file mode 100644
index 000000000..05551c26d
--- /dev/null
+++
b/parquet-variant/src/main/java/org/apache/parquet/variant/VariantJsonParser.java
@@ -0,0 +1,206 @@
+/*
+ * 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.parquet.variant;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.StreamReadConstraints;
+import com.fasterxml.jackson.core.exc.InputCoercionException;
+import java.io.IOException;
+import java.math.BigDecimal;
+
+/**
+ * Parses JSON into {@link Variant} values using Jackson streaming.
+ *
+ * <p>This class isolates the Jackson dependency from {@link VariantBuilder},
+ * so that core variant construction does not require Jackson on the classpath.
+ *
+ * <p>Ported from Apache Spark's {@code VariantBuilder.parseJson}.
+ */
+public final class VariantJsonParser {
+
+ private static final JsonFactory JSON_FACTORY = JsonFactory.builder()
+ .streamReadConstraints(StreamReadConstraints.builder()
+ .maxNestingDepth(500)
+ .maxStringLength(10_000_000)
+ .maxDocumentLength(50_000_000L)
+ .build())
+ .build();
+
+ private VariantJsonParser() {}
+
+ /**
+ * Parses a JSON string and returns the corresponding {@link Variant}.
+ *
+ * <p>Uses Jackson streaming parser for single-pass conversion
+ * with no intermediate tree. Number handling preserves precision:
+ * integers use the smallest fitting type, floating-point numbers
+ * prefer decimal encoding (no scientific notation) and fall back
+ * to double.
+ *
+ * @param json the JSON string to parse
+ * @return the parsed Variant
+ * @throws IOException if the JSON is malformed or an I/O error occurs
+ */
+ public static Variant parseJson(String json) throws IOException {
+ try (JsonParser parser = JSON_FACTORY.createParser(json)) {
+ parser.nextToken();
+ return parseJson(parser);
+ }
+ }
+
+ /**
+ * Parses a JSON value from an already-positioned {@link JsonParser}
+ * and returns the corresponding {@link Variant}. The parser must
+ * have its current token set (i.e., {@code parser.nextToken()}
+ * or equivalent must have been called).
+ *
+ * @param parser a positioned Jackson JsonParser
+ * @return the parsed Variant
+ * @throws IOException if the JSON is malformed or an I/O error occurs
+ */
+ public static Variant parseJson(JsonParser parser) throws IOException {
+ VariantBuilder builder = new VariantBuilder();
+ buildJson(builder, parser);
+ return builder.build();
+ }
+
+ /**
+ * Recursively builds a Variant value from the current position of a
+ * Jackson streaming parser. Handles objects, arrays, strings, numbers
+ * (int/long/decimal/double), booleans, and null.
+ */
+ private static void buildJson(VariantBuilder builder, JsonParser parser)
throws IOException {
+ JsonToken token = parser.currentToken();
+ if (token == null) {
+ throw new JsonParseException(parser, "Unexpected null token");
+ }
+ switch (token) {
+ case START_OBJECT:
+ buildJsonObject(builder, parser);
+ break;
+ case START_ARRAY:
+ buildJsonArray(builder, parser);
+ break;
+ case VALUE_STRING:
+ builder.appendString(parser.getText());
+ break;
+ case VALUE_NUMBER_INT:
+ buildJsonInteger(builder, parser);
+ break;
+ case VALUE_NUMBER_FLOAT:
+ buildJsonFloat(builder, parser);
+ break;
+ case VALUE_TRUE:
+ builder.appendBoolean(true);
+ break;
+ case VALUE_FALSE:
+ builder.appendBoolean(false);
+ break;
+ case VALUE_NULL:
+ builder.appendNull();
+ break;
+ default:
+ throw new JsonParseException(parser, "Unexpected token " + token);
+ }
+ }
+
+ /**
+ * Builds a Variant object from the current JSON object token.
+ *
+ * <p>Iterates over each key-value pair in the JSON object. For each value,
+ * this method co-recurses into {@link #buildJson(VariantBuilder,
JsonParser)}
+ * to handle arbitrarily nested structures (objects within objects, arrays
+ * within objects, etc.).
+ */
+ private static void buildJsonObject(VariantBuilder builder, JsonParser
parser) throws IOException {
+ VariantObjectBuilder obj = builder.startObject();
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ obj.appendKey(parser.currentName());
+ parser.nextToken();
+ buildJson(obj, parser);
+ }
+ builder.endObject();
+ }
+
+ /**
+ * Builds a Variant array from the current JSON array token.
+ *
+ * <p>Iterates over each element in the JSON array. For each element,
+ * this method co-recurses into {@link #buildJson(VariantBuilder,
JsonParser)}
+ * to handle arbitrarily nested structures (objects within arrays, arrays
+ * within arrays, etc.).
+ */
+ private static void buildJsonArray(VariantBuilder builder, JsonParser
parser) throws IOException {
+ VariantArrayBuilder arr = builder.startArray();
+ while (parser.nextToken() != JsonToken.END_ARRAY) {
+ buildJson(arr, parser);
+ }
+ builder.endArray();
+ }
+
+ private static void buildJsonInteger(VariantBuilder builder, JsonParser
parser) throws IOException {
+ try {
+ appendSmallestLong(builder, parser.getLongValue());
+ } catch (InputCoercionException ignored) {
+ buildJsonFloat(builder, parser);
+ }
+ }
+
+ private static void buildJsonFloat(VariantBuilder builder, JsonParser
parser) throws IOException {
+ if (!tryAppendDecimal(builder, parser.getText())) {
+ builder.appendDouble(parser.getDoubleValue());
+ }
+ }
+
+ /**
+ * Appends a long value using the smallest integer type that fits.
+ */
+ private static void appendSmallestLong(VariantBuilder builder, long l) {
+ if (l == (byte) l) {
+ builder.appendByte((byte) l);
+ } else if (l == (short) l) {
+ builder.appendShort((short) l);
+ } else if (l == (int) l) {
+ builder.appendInt((int) l);
+ } else {
+ builder.appendLong(l);
+ }
+ }
+
+ /**
+ * Tries to parse a number string as a decimal. Only accepts plain
+ * decimal format (digits, minus, dot -- no scientific notation).
+ * Returns true if the number was successfully appended as a decimal.
+ */
+ private static boolean tryAppendDecimal(VariantBuilder builder, String
input) {
+ for (int i = 0; i < input.length(); i++) {
+ char ch = input.charAt(i);
+ if (ch != '-' && ch != '.' && !(ch >= '0' && ch <= '9')) {
+ return false;
+ }
+ }
+ BigDecimal d = new BigDecimal(input);
+ if (d.scale() <= VariantUtil.MAX_DECIMAL16_PRECISION && d.precision() <=
VariantUtil.MAX_DECIMAL16_PRECISION) {
+ builder.appendDecimal(d);
+ return true;
+ }
+ return false;
+ }
+}
diff --git
a/parquet-variant/src/test/java/org/apache/parquet/variant/TestVariantParseJson.java
b/parquet-variant/src/test/java/org/apache/parquet/variant/TestVariantParseJson.java
new file mode 100644
index 000000000..f2697a00f
--- /dev/null
+++
b/parquet-variant/src/test/java/org/apache/parquet/variant/TestVariantParseJson.java
@@ -0,0 +1,343 @@
+/*
+ * 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.parquet.variant;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import java.io.IOException;
+import java.math.BigDecimal;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestVariantParseJson {
+
+ @Test
+ public void testParseNull() throws IOException {
+ Variant v = VariantJsonParser.parseJson("null");
+ Assert.assertEquals(Variant.Type.NULL, v.getType());
+ }
+
+ @Test
+ public void testParseTrue() throws IOException {
+ Variant v = VariantJsonParser.parseJson("true");
+ Assert.assertEquals(Variant.Type.BOOLEAN, v.getType());
+ Assert.assertTrue(v.getBoolean());
+ }
+
+ @Test
+ public void testParseFalse() throws IOException {
+ Variant v = VariantJsonParser.parseJson("false");
+ Assert.assertEquals(Variant.Type.BOOLEAN, v.getType());
+ Assert.assertFalse(v.getBoolean());
+ }
+
+ @Test
+ public void testParseString() throws IOException {
+ Variant v = VariantJsonParser.parseJson("\"hello world\"");
+ Assert.assertEquals(Variant.Type.STRING, v.getType());
+ Assert.assertEquals("hello world", v.getString());
+ }
+
+ @Test
+ public void testParseSmallInteger() throws IOException {
+ Variant v = VariantJsonParser.parseJson("42");
+ Assert.assertEquals(Variant.Type.BYTE, v.getType());
+ Assert.assertEquals(42, v.getLong());
+ }
+
+ @Test
+ public void testParseShortInteger() throws IOException {
+ Variant v = VariantJsonParser.parseJson("1000");
+ Assert.assertEquals(Variant.Type.SHORT, v.getType());
+ Assert.assertEquals(1000, v.getLong());
+ }
+
+ @Test
+ public void testParseIntInteger() throws IOException {
+ Variant v = VariantJsonParser.parseJson("100000");
+ Assert.assertEquals(Variant.Type.INT, v.getType());
+ Assert.assertEquals(100000, v.getLong());
+ }
+
+ @Test
+ public void testParseLongInteger() throws IOException {
+ Variant v = VariantJsonParser.parseJson("9999999999");
+ Assert.assertEquals(Variant.Type.LONG, v.getType());
+ Assert.assertEquals(9999999999L, v.getLong());
+ }
+
+ @Test
+ public void testParseDecimalFloat() throws IOException {
+ Variant v = VariantJsonParser.parseJson("3.14");
+ Variant.Type type = v.getType();
+ Assert.assertTrue(
+ "Expected decimal type, got " + type,
+ type == Variant.Type.DECIMAL4 || type == Variant.Type.DECIMAL8 || type
== Variant.Type.DECIMAL16);
+ Assert.assertEquals(0, new BigDecimal("3.14").compareTo(v.getDecimal()));
+ }
+
+ @Test
+ public void testParseScientificNotationDouble() throws IOException {
+ Variant v = VariantJsonParser.parseJson("1.5e10");
+ Assert.assertEquals(Variant.Type.DOUBLE, v.getType());
+ Assert.assertEquals(1.5e10, v.getDouble(), 0.001);
+ }
+
+ @Test
+ public void testParseLargeIntegerAsDecimal() throws IOException {
+ String bigNum = "99999999999999999999";
+ Variant v = VariantJsonParser.parseJson(bigNum);
+ Variant.Type type = v.getType();
+ Assert.assertTrue(
+ "Expected decimal type for big integer, got " + type,
+ type == Variant.Type.DECIMAL4 || type == Variant.Type.DECIMAL8 || type
== Variant.Type.DECIMAL16);
+ Assert.assertEquals(0, new BigDecimal(bigNum).compareTo(v.getDecimal()));
+ }
+
+ @Test
+ public void testParseNegativeInteger() throws IOException {
+ Variant v = VariantJsonParser.parseJson("-100");
+ Assert.assertEquals(-100, v.getLong());
+ }
+
+ @Test
+ public void testParseNegativeDecimal() throws IOException {
+ Variant v = VariantJsonParser.parseJson("-99.99");
+ Assert.assertEquals(0, new BigDecimal("-99.99").compareTo(v.getDecimal()));
+ }
+
+ @Test
+ public void testParseZero() throws IOException {
+ Variant v = VariantJsonParser.parseJson("0");
+ Assert.assertEquals(Variant.Type.BYTE, v.getType());
+ Assert.assertEquals(0, v.getLong());
+ }
+
+ @Test
+ public void testParseEmptyObject() throws IOException {
+ Variant v = VariantJsonParser.parseJson("{}");
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ Assert.assertEquals(0, v.numObjectElements());
+ }
+
+ @Test
+ public void testParseSimpleObject() throws IOException {
+ Variant v = VariantJsonParser.parseJson("{\"name\":\"John\",\"age\":30}");
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ Assert.assertEquals(2, v.numObjectElements());
+ Assert.assertEquals("John", v.getFieldByKey("name").getString());
+ Assert.assertEquals(30, v.getFieldByKey("age").getLong());
+ }
+
+ @Test
+ public void testParseNestedObject() throws IOException {
+ Variant v =
VariantJsonParser.parseJson("{\"user\":{\"id\":100,\"country\":\"US\"},\"active\":true}");
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ Variant user = v.getFieldByKey("user");
+ Assert.assertEquals(Variant.Type.OBJECT, user.getType());
+ Assert.assertEquals(100, user.getFieldByKey("id").getLong());
+ Assert.assertEquals("US", user.getFieldByKey("country").getString());
+ Assert.assertTrue(v.getFieldByKey("active").getBoolean());
+ }
+
+ @Test
+ public void testParseEmptyArray() throws IOException {
+ Variant v = VariantJsonParser.parseJson("[]");
+ Assert.assertEquals(Variant.Type.ARRAY, v.getType());
+ Assert.assertEquals(0, v.numArrayElements());
+ }
+
+ @Test
+ public void testParseSimpleArray() throws IOException {
+ Variant v = VariantJsonParser.parseJson("[1,2,3,\"four\"]");
+ Assert.assertEquals(Variant.Type.ARRAY, v.getType());
+ Assert.assertEquals(4, v.numArrayElements());
+ Assert.assertEquals(1, v.getElementAtIndex(0).getLong());
+ Assert.assertEquals(2, v.getElementAtIndex(1).getLong());
+ Assert.assertEquals(3, v.getElementAtIndex(2).getLong());
+ Assert.assertEquals("four", v.getElementAtIndex(3).getString());
+ }
+
+ @Test
+ public void testParseNestedArray() throws IOException {
+ Variant v = VariantJsonParser.parseJson("[[1,2],[3,4]]");
+ Assert.assertEquals(Variant.Type.ARRAY, v.getType());
+ Assert.assertEquals(2, v.numArrayElements());
+ Variant inner = v.getElementAtIndex(0);
+ Assert.assertEquals(Variant.Type.ARRAY, inner.getType());
+ Assert.assertEquals(1, inner.getElementAtIndex(0).getLong());
+ Assert.assertEquals(2, inner.getElementAtIndex(1).getLong());
+ }
+
+ @Test
+ public void testParseMixedArray() throws IOException {
+ Variant v = VariantJsonParser.parseJson("[1,\"two\",true,null,3.14]");
+ Assert.assertEquals(Variant.Type.ARRAY, v.getType());
+ Assert.assertEquals(5, v.numArrayElements());
+ Assert.assertEquals(1, v.getElementAtIndex(0).getLong());
+ Assert.assertEquals("two", v.getElementAtIndex(1).getString());
+ Assert.assertTrue(v.getElementAtIndex(2).getBoolean());
+ Assert.assertEquals(Variant.Type.NULL, v.getElementAtIndex(3).getType());
+ Assert.assertEquals(
+ 0, new
BigDecimal("3.14").compareTo(v.getElementAtIndex(4).getDecimal()));
+ }
+
+ @Test
+ public void testParseObjectWithNullValue() throws IOException {
+ Variant v = VariantJsonParser.parseJson("{\"key\":null}");
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ Assert.assertEquals(Variant.Type.NULL, v.getFieldByKey("key").getType());
+ }
+
+ @Test
+ public void testParseComplexDocument() throws IOException {
+ String json = "{\"userId\":12345,\"events\":["
+ + "{\"eType\":\"login\",\"ts\":\"2026-01-15T10:30:00Z\"},"
+ + "{\"eType\":\"purchase\",\"amount\":99.99}"
+ + "]}";
+ Variant v = VariantJsonParser.parseJson(json);
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ Assert.assertEquals(12345, v.getFieldByKey("userId").getLong());
+ Variant events = v.getFieldByKey("events");
+ Assert.assertEquals(Variant.Type.ARRAY, events.getType());
+ Assert.assertEquals(2, events.numArrayElements());
+ Assert.assertEquals(
+ "login",
events.getElementAtIndex(0).getFieldByKey("eType").getString());
+ Assert.assertEquals(
+ 0,
+ new BigDecimal("99.99")
+ .compareTo(events.getElementAtIndex(1)
+ .getFieldByKey("amount")
+ .getDecimal()));
+ }
+
+ @Test
+ public void testParseEmptyString() throws IOException {
+ Variant v = VariantJsonParser.parseJson("\"\"");
+ Assert.assertEquals(Variant.Type.STRING, v.getType());
+ Assert.assertEquals("", v.getString());
+ }
+
+ @Test
+ public void testParseUnicodeString() throws IOException {
+ Variant v = VariantJsonParser.parseJson("\"\\u00e9l\\u00e8ve\"");
+ Assert.assertEquals(Variant.Type.STRING, v.getType());
+ Assert.assertEquals("\u00e9l\u00e8ve", v.getString());
+ }
+
+ @Test
+ public void testParseEscapedString() throws IOException {
+ Variant v = VariantJsonParser.parseJson("\"hello\\nworld\"");
+ Assert.assertEquals(Variant.Type.STRING, v.getType());
+ Assert.assertEquals("hello\nworld", v.getString());
+ }
+
+ @Test(expected = IOException.class)
+ public void testParseMalformedJson() throws IOException {
+ VariantJsonParser.parseJson("{invalid");
+ }
+
+ @Test(expected = IOException.class)
+ public void testParseIncompleteObject() throws IOException {
+ VariantJsonParser.parseJson("{\"key\":");
+ }
+
+ @Test
+ public void testParseJsonWithParser() throws IOException {
+ JsonFactory factory = new JsonFactory();
+ try (JsonParser parser = factory.createParser("{\"a\":1}")) {
+ parser.nextToken();
+ Variant v = VariantJsonParser.parseJson(parser);
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ Assert.assertEquals(1, v.getFieldByKey("a").getLong());
+ }
+ }
+
+ @Test
+ public void testParseDuplicateKeysLastWins() throws IOException {
+ Variant v = VariantJsonParser.parseJson("{\"k\":1,\"k\":2}");
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ Assert.assertEquals(1, v.numObjectElements());
+ Assert.assertEquals(2, v.getFieldByKey("k").getLong());
+ }
+
+ @Test
+ public void testParseDeeplyNested() throws IOException {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 20; i++) {
+ sb.append("{\"n\":");
+ }
+ sb.append("42");
+ for (int i = 0; i < 20; i++) {
+ sb.append("}");
+ }
+ Variant v = VariantJsonParser.parseJson(sb.toString());
+ for (int i = 0; i < 20; i++) {
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ v = v.getFieldByKey("n");
+ }
+ Assert.assertEquals(42, v.getLong());
+ }
+
+ @Test
+ public void testObjectKeysSorted() throws IOException {
+ Variant v = VariantJsonParser.parseJson("{\"c\":3,\"a\":1,\"b\":2}");
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ Assert.assertEquals(3, v.numObjectElements());
+ Assert.assertEquals("a", v.getFieldAtIndex(0).key);
+ Assert.assertEquals("b", v.getFieldAtIndex(1).key);
+ Assert.assertEquals("c", v.getFieldAtIndex(2).key);
+ }
+
+ @Test(expected = IOException.class)
+ public void testParseEmptyInput() throws IOException {
+ VariantJsonParser.parseJson("");
+ }
+
+ @Test(expected = IOException.class)
+ public void testParseNotJson() throws IOException {
+ VariantJsonParser.parseJson("not json at all");
+ }
+
+ @Test(expected = IOException.class)
+ public void testParseIncompleteArray() throws IOException {
+ VariantJsonParser.parseJson("[1, 2,");
+ }
+
+ @Test
+ public void testParseLargeJsonWithManyValues() throws IOException {
+ StringBuilder sb = new StringBuilder("{");
+ int numKeys = 1000;
+ for (int i = 0; i < numKeys; i++) {
+ if (i > 0) {
+ sb.append(",");
+ }
+ sb.append("\"key").append(i).append("\":").append(i);
+ }
+ sb.append("}");
+
+ Variant v = VariantJsonParser.parseJson(sb.toString());
+ Assert.assertEquals(Variant.Type.OBJECT, v.getType());
+ Assert.assertEquals(numKeys, v.numObjectElements());
+ // Spot-check a few values
+ Assert.assertEquals(0, v.getFieldByKey("key0").getLong());
+ Assert.assertEquals(500, v.getFieldByKey("key500").getLong());
+ Assert.assertEquals(999, v.getFieldByKey("key999").getLong());
+ }
+}