This is an automated email from the ASF dual-hosted git repository.
kharekartik pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 78613ef3150 [spi] Fix Schema/TableConfigs serialization with JsonValue
annotation (#17558)
78613ef3150 is described below
commit 78613ef31504bc5c9c6ffc8831fc29f8598b0af6
Author: Anshul Singh <[email protected]>
AuthorDate: Tue Jan 27 12:59:02 2026 +0530
[spi] Fix Schema/TableConfigs serialization with JsonValue annotation
(#17558)
---
.../org/apache/pinot/spi/config/TableConfigs.java | 4 +-
.../java/org/apache/pinot/spi/data/Schema.java | 2 +
.../spi/config/TableConfigsSerializationTest.java | 292 +++++++++++++++
.../pinot/spi/data/SchemaSerializationTest.java | 410 +++++++++++++++++++++
4 files changed, 707 insertions(+), 1 deletion(-)
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/config/TableConfigs.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/config/TableConfigs.java
index 6f5103a7476..d7f5924df35 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/config/TableConfigs.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/config/TableConfigs.java
@@ -20,6 +20,7 @@ package org.apache.pinot.spi.config;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Preconditions;
@@ -83,7 +84,8 @@ public class TableConfigs extends BaseJsonConfig {
return _realtime;
}
- private ObjectNode toJsonObject() {
+ @JsonValue
+ public ObjectNode toJsonObject() {
ObjectNode tableConfigsObjectNode = JsonUtils.newObjectNode();
tableConfigsObjectNode.put("tableName", _tableName);
tableConfigsObjectNode.set("schema", _schema.toJsonObject());
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/data/Schema.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/Schema.java
index 8956ff9756b..d82f32ce208 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/data/Schema.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/data/Schema.java
@@ -20,6 +20,7 @@ package org.apache.pinot.spi.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -503,6 +504,7 @@ public final class Schema implements Serializable {
/**
* Returns a json representation of the schema.
*/
+ @JsonValue
public ObjectNode toJsonObject() {
ObjectNode jsonObject = JsonUtils.newObjectNode();
jsonObject.put("schemaName", _schemaName);
diff --git
a/pinot-spi/src/test/java/org/apache/pinot/spi/config/TableConfigsSerializationTest.java
b/pinot-spi/src/test/java/org/apache/pinot/spi/config/TableConfigsSerializationTest.java
new file mode 100644
index 00000000000..9c546acd609
--- /dev/null
+++
b/pinot-spi/src/test/java/org/apache/pinot/spi/config/TableConfigsSerializationTest.java
@@ -0,0 +1,292 @@
+/**
+ * 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.pinot.spi.config;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+
+/**
+ * Unit tests for TableConfigs serialization with @JsonValue annotation.
+ * These tests verify that Jackson serialization uses the toJsonObject() method
+ * which produces a minimal, canonical JSON format.
+ */
+public class TableConfigsSerializationTest {
+
+ private static final String TEST_TABLE_NAME = "testTable";
+
+ private Schema createTestSchema() {
+ return new Schema.SchemaBuilder()
+ .setSchemaName(TEST_TABLE_NAME)
+ .addSingleValueDimension("dim1", FieldSpec.DataType.STRING)
+ .addMetric("metric1", FieldSpec.DataType.LONG)
+ .addDateTime("ts", FieldSpec.DataType.LONG, "1:MILLISECONDS:EPOCH",
"1:MILLISECONDS")
+ .build();
+ }
+
+ private TableConfig createOfflineTableConfig() {
+ return new TableConfigBuilder(TableType.OFFLINE)
+ .setTableName(TEST_TABLE_NAME)
+ .setNumReplicas(1)
+ .build();
+ }
+
+ private TableConfig createRealtimeTableConfig() {
+ return new TableConfigBuilder(TableType.REALTIME)
+ .setTableName(TEST_TABLE_NAME)
+ .setNumReplicas(1)
+ .setStreamConfigs(java.util.Map.of(
+ "streamType", "kafka",
+ "stream.kafka.topic.name", "testTopic",
+ "stream.kafka.broker.list", "localhost:9092",
+ "stream.kafka.decoder.class.name",
"org.apache.pinot.plugin.inputformat.json.JSONMessageDecoder"
+ ))
+ .build();
+ }
+
+ /**
+ * Tests that TableConfigs serialization uses toJsonObject() format via
@JsonValue.
+ */
+ @Test
+ public void testTableConfigsSerializationUsesToJsonObject()
+ throws Exception {
+ final Schema schema = createTestSchema();
+ final TableConfig offlineConfig = createOfflineTableConfig();
+
+ final TableConfigs tableConfigs = new TableConfigs(TEST_TABLE_NAME,
schema, offlineConfig, null);
+
+ // Serialize using Jackson
+ final String jsonString = JsonUtils.objectToString(tableConfigs);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ // Verify structure
+ Assert.assertTrue(jsonNode.has("tableName"));
+ Assert.assertEquals(jsonNode.get("tableName").asText(), TEST_TABLE_NAME);
+
+ Assert.assertTrue(jsonNode.has("schema"));
+ Assert.assertTrue(jsonNode.has("offline"));
+ Assert.assertFalse(jsonNode.has("realtime"), "realtime should not be
present when null");
+ }
+
+ /**
+ * Tests that the embedded Schema in TableConfigs uses toJsonObject() format.
+ */
+ @Test
+ public void testTableConfigsSchemaSerializationFormat()
+ throws Exception {
+ final Schema schema = createTestSchema();
+ final TableConfig offlineConfig = createOfflineTableConfig();
+
+ final TableConfigs tableConfigs = new TableConfigs(TEST_TABLE_NAME,
schema, offlineConfig, null);
+
+ final String jsonString = JsonUtils.objectToString(tableConfigs);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ // Get the schema node
+ final JsonNode schemaNode = jsonNode.get("schema");
+ Assert.assertNotNull(schemaNode);
+
+ // Verify schema uses toJsonObject() format (no defaultNullValueString)
+ final String schemaString = schemaNode.toString();
+ Assert.assertFalse(schemaString.contains("defaultNullValueString"),
+ "Schema within TableConfigs should not contain
defaultNullValueString");
+
+ // Verify schemaName is present
+ Assert.assertTrue(schemaNode.has("schemaName"));
+ Assert.assertEquals(schemaNode.get("schemaName").asText(),
TEST_TABLE_NAME);
+
+ // Verify enableColumnBasedNullHandling is present
+ Assert.assertTrue(schemaNode.has("enableColumnBasedNullHandling"));
+
+ // Verify dimensionFieldSpecs uses minimal format
+ final JsonNode dimSpecs = schemaNode.get("dimensionFieldSpecs");
+ Assert.assertNotNull(dimSpecs);
+ Assert.assertEquals(dimSpecs.size(), 1);
+
+ final JsonNode dimSpec = dimSpecs.get(0);
+ Assert.assertFalse(dimSpec.has("defaultNullValue"),
+ "Default null value should be omitted for STRING dimension");
+ Assert.assertFalse(dimSpec.has("notNull"),
+ "notNull should be omitted when false (default)");
+ Assert.assertFalse(dimSpec.has("singleValueField"),
+ "singleValueField should be omitted when true (default)");
+ }
+
+ /**
+ * Tests that TableConfigs with both offline and realtime configs serializes
correctly.
+ */
+ @Test
+ public void testTableConfigsWithBothTableTypes()
+ throws Exception {
+ final Schema schema = createTestSchema();
+ final TableConfig offlineConfig = createOfflineTableConfig();
+ final TableConfig realtimeConfig = createRealtimeTableConfig();
+
+ final TableConfigs tableConfigs = new TableConfigs(TEST_TABLE_NAME,
schema, offlineConfig, realtimeConfig);
+
+ final String jsonString = JsonUtils.objectToString(tableConfigs);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ Assert.assertTrue(jsonNode.has("tableName"));
+ Assert.assertTrue(jsonNode.has("schema"));
+ Assert.assertTrue(jsonNode.has("offline"));
+ Assert.assertTrue(jsonNode.has("realtime"));
+
+ // Verify offline config
+ final JsonNode offlineNode = jsonNode.get("offline");
+ Assert.assertEquals(offlineNode.get("tableType").asText(), "OFFLINE");
+
+ // Verify realtime config
+ final JsonNode realtimeNode = jsonNode.get("realtime");
+ Assert.assertEquals(realtimeNode.get("tableType").asText(), "REALTIME");
+ }
+
+ /**
+ * Tests that Jackson serialization output matches toJsonObject() output.
+ */
+ @Test
+ public void testJacksonSerializationMatchesToJsonObject()
+ throws Exception {
+ final Schema schema = createTestSchema();
+ final TableConfig offlineConfig = createOfflineTableConfig();
+
+ final TableConfigs tableConfigs = new TableConfigs(TEST_TABLE_NAME,
schema, offlineConfig, null);
+
+ // Get JSON from Jackson serialization
+ final String jacksonJson = JsonUtils.objectToString(tableConfigs);
+ final JsonNode jacksonNode = JsonUtils.stringToJsonNode(jacksonJson);
+
+ // Get JSON from toJsonObject()
+ final JsonNode toJsonObjectNode = tableConfigs.toJsonObject();
+
+ // They should be equal
+ Assert.assertEquals(jacksonNode, toJsonObjectNode,
+ "Jackson serialization should match toJsonObject() output");
+ }
+
+ /**
+ * Tests round-trip serialization/deserialization of TableConfigs.
+ */
+ @Test
+ public void testRoundTripSerialization()
+ throws Exception {
+ final Schema schema = createTestSchema();
+ final TableConfig offlineConfig = createOfflineTableConfig();
+
+ final TableConfigs original = new TableConfigs(TEST_TABLE_NAME, schema,
offlineConfig, null);
+
+ // Serialize
+ final String jsonString = JsonUtils.objectToString(original);
+
+ // Deserialize
+ final TableConfigs deserialized = JsonUtils.stringToObject(jsonString,
TableConfigs.class);
+
+ // Verify
+ Assert.assertEquals(deserialized.getTableName(), TEST_TABLE_NAME);
+ Assert.assertNotNull(deserialized.getSchema());
+ Assert.assertEquals(deserialized.getSchema().getSchemaName(),
TEST_TABLE_NAME);
+ Assert.assertNotNull(deserialized.getOffline());
+ Assert.assertNull(deserialized.getRealtime());
+
+ // Verify schema fields
+ Assert.assertNotNull(deserialized.getSchema().getDimensionSpec("dim1"));
+ Assert.assertNotNull(deserialized.getSchema().getMetricSpec("metric1"));
+ Assert.assertNotNull(deserialized.getSchema().getDateTimeSpec("ts"));
+ }
+
+ /**
+ * Tests that a fresh ObjectMapper produces the same serialization.
+ * This verifies that @JsonValue works with any ObjectMapper.
+ */
+ @Test
+ public void testJsonValueWorksWithFreshObjectMapper()
+ throws Exception {
+ final Schema schema = createTestSchema();
+ final TableConfig offlineConfig = createOfflineTableConfig();
+
+ final TableConfigs tableConfigs = new TableConfigs(TEST_TABLE_NAME,
schema, offlineConfig, null);
+
+ // Use a fresh ObjectMapper
+ final ObjectMapper freshMapper = new ObjectMapper();
+ final String freshMapperJson =
freshMapper.writeValueAsString(tableConfigs);
+
+ // Should produce toJsonObject() format
+ Assert.assertFalse(freshMapperJson.contains("defaultNullValueString"),
+ "Fresh ObjectMapper should use @JsonValue and not include
defaultNullValueString");
+
+ // Verify it's valid JSON and has expected structure
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(freshMapperJson);
+ Assert.assertTrue(jsonNode.has("tableName"));
+ Assert.assertTrue(jsonNode.has("schema"));
+ Assert.assertTrue(jsonNode.has("offline"));
+ }
+
+ /**
+ * Tests that toJsonString() and Jackson serialization produce the same
result.
+ */
+ @Test
+ public void testToJsonStringMatchesJacksonSerialization()
+ throws Exception {
+ final Schema schema = createTestSchema();
+ final TableConfig offlineConfig = createOfflineTableConfig();
+
+ final TableConfigs tableConfigs = new TableConfigs(TEST_TABLE_NAME,
schema, offlineConfig, null);
+
+ // Get JSON from toJsonString()
+ final String toJsonStringResult = tableConfigs.toJsonString();
+
+ // Get JSON from Jackson
+ final String jacksonResult = JsonUtils.objectToString(tableConfigs);
+
+ // Parse both and compare (to handle whitespace differences)
+ final JsonNode toJsonStringNode =
JsonUtils.stringToJsonNode(toJsonStringResult);
+ final JsonNode jacksonNode = JsonUtils.stringToJsonNode(jacksonResult);
+
+ Assert.assertEquals(jacksonNode, toJsonStringNode,
+ "toJsonString() and Jackson serialization should produce equivalent
JSON");
+ }
+
+ /**
+ * Tests serialization with realtime-only TableConfigs.
+ */
+ @Test
+ public void testRealtimeOnlyTableConfigs()
+ throws Exception {
+ final Schema schema = createTestSchema();
+ final TableConfig realtimeConfig = createRealtimeTableConfig();
+
+ final TableConfigs tableConfigs = new TableConfigs(TEST_TABLE_NAME,
schema, null, realtimeConfig);
+
+ final String jsonString = JsonUtils.objectToString(tableConfigs);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ Assert.assertTrue(jsonNode.has("tableName"));
+ Assert.assertTrue(jsonNode.has("schema"));
+ Assert.assertFalse(jsonNode.has("offline"), "offline should not be present
when null");
+ Assert.assertTrue(jsonNode.has("realtime"));
+ }
+}
diff --git
a/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaSerializationTest.java
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaSerializationTest.java
new file mode 100644
index 00000000000..f6a8b7ce535
--- /dev/null
+++
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/SchemaSerializationTest.java
@@ -0,0 +1,410 @@
+/**
+ * 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.pinot.spi.data;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Map;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+import org.testng.collections.Lists;
+
+
+/**
+ * Unit tests for Schema serialization with @JsonValue annotation.
+ * These tests verify that Jackson serialization uses the toJsonObject() method
+ * which produces a minimal, canonical JSON format.
+ */
+public class SchemaSerializationTest {
+
+ /**
+ * Tests that Jackson serialization uses toJsonObject() format via
@JsonValue annotation.
+ * This ensures that defaultNullValueString is never included in serialized
output.
+ */
+ @Test
+ public void testJsonValueSerializationOmitsDefaultNullValueString()
+ throws Exception {
+ final Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .addSingleValueDimension("dim1", FieldSpec.DataType.STRING)
+ .addMetric("metric1", FieldSpec.DataType.LONG)
+ .build();
+
+ // Serialize using Jackson (which should use @JsonValue -> toJsonObject())
+ final String jsonString = JsonUtils.objectToString(schema);
+
+ // Verify that defaultNullValueString is NOT present in the output
+ Assert.assertFalse(jsonString.contains("defaultNullValueString"),
+ "defaultNullValueString should not be present in serialized output");
+
+ // Verify it can be deserialized back
+ final Schema deserializedSchema = Schema.fromString(jsonString);
+ Assert.assertEquals(deserializedSchema.getSchemaName(), "testSchema");
+ Assert.assertNotNull(deserializedSchema.getDimensionSpec("dim1"));
+ Assert.assertNotNull(deserializedSchema.getMetricSpec("metric1"));
+ }
+
+ /**
+ * Tests that Jackson serialization omits default values for fields.
+ */
+ @Test
+ public void testJsonValueSerializationOmitsDefaultValues()
+ throws Exception {
+ final Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .addSingleValueDimension("dim1", FieldSpec.DataType.STRING) // default
null value = "null"
+ .addMetric("metric1", FieldSpec.DataType.DOUBLE) // default null value
= 0.0
+ .build();
+
+ final String jsonString = JsonUtils.objectToString(schema);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ // Check dimension field spec - should not have defaultNullValue since
"null" is the default
+ final JsonNode dimSpecs = jsonNode.get("dimensionFieldSpecs");
+ Assert.assertNotNull(dimSpecs);
+ Assert.assertEquals(dimSpecs.size(), 1);
+ final JsonNode dimSpec = dimSpecs.get(0);
+ Assert.assertFalse(dimSpec.has("defaultNullValue"),
+ "defaultNullValue should not be present for STRING dimension with
default value");
+ Assert.assertFalse(dimSpec.has("notNull"),
+ "notNull should not be present when false (default)");
+ Assert.assertFalse(dimSpec.has("singleValueField"),
+ "singleValueField should not be present when true (default)");
+ Assert.assertFalse(dimSpec.has("allowTrailingZeros"),
+ "allowTrailingZeros should not be present when false (default)");
+
+ // Check metric field spec - should not have defaultNullValue since 0.0 is
the default
+ final JsonNode metricSpecs = jsonNode.get("metricFieldSpecs");
+ Assert.assertNotNull(metricSpecs);
+ Assert.assertEquals(metricSpecs.size(), 1);
+ final JsonNode metricSpec = metricSpecs.get(0);
+ Assert.assertFalse(metricSpec.has("defaultNullValue"),
+ "defaultNullValue should not be present for DOUBLE metric with default
value");
+ }
+
+ /**
+ * Tests that Jackson serialization includes non-default values.
+ */
+ @Test
+ public void testJsonValueSerializationIncludesNonDefaultValues()
+ throws Exception {
+ final Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .addSingleValueDimension("dim1", FieldSpec.DataType.STRING,
"custom_default")
+ .addMetric("metric1", FieldSpec.DataType.DOUBLE, 99.9)
+ .addMultiValueDimension("mvDim", FieldSpec.DataType.INT)
+ .build();
+
+ final String jsonString = JsonUtils.objectToString(schema);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ // Check dimension with custom default
+ final JsonNode dimSpecs = jsonNode.get("dimensionFieldSpecs");
+ JsonNode dim1 = null;
+ JsonNode mvDim = null;
+ for (int i = 0; i < dimSpecs.size(); i++) {
+ final JsonNode spec = dimSpecs.get(i);
+ if ("dim1".equals(spec.get("name").asText())) {
+ dim1 = spec;
+ } else if ("mvDim".equals(spec.get("name").asText())) {
+ mvDim = spec;
+ }
+ }
+
+ Assert.assertNotNull(dim1);
+ Assert.assertTrue(dim1.has("defaultNullValue"),
+ "defaultNullValue should be present for non-default value");
+ Assert.assertEquals(dim1.get("defaultNullValue").asText(),
"custom_default");
+
+ // Check multi-value dimension has singleValueField: false
+ Assert.assertNotNull(mvDim);
+ Assert.assertTrue(mvDim.has("singleValueField"),
+ "singleValueField should be present when false (non-default)");
+ Assert.assertFalse(mvDim.get("singleValueField").asBoolean());
+
+ // Check metric with custom default
+ final JsonNode metricSpecs = jsonNode.get("metricFieldSpecs");
+ Assert.assertNotNull(metricSpecs);
+ final JsonNode metric1 = metricSpecs.get(0);
+ Assert.assertTrue(metric1.has("defaultNullValue"),
+ "defaultNullValue should be present for non-default value");
+ Assert.assertEquals(metric1.get("defaultNullValue").asDouble(), 99.9);
+ }
+
+ /**
+ * Tests that empty field spec arrays are omitted from serialization.
+ */
+ @Test
+ public void testJsonValueSerializationOmitsEmptyArrays()
+ throws Exception {
+ final Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .addSingleValueDimension("dim1", FieldSpec.DataType.STRING)
+ .build();
+
+ final String jsonString = JsonUtils.objectToString(schema);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ // Should have dimensionFieldSpecs
+ Assert.assertTrue(jsonNode.has("dimensionFieldSpecs"));
+
+ // Should NOT have empty metricFieldSpecs, dateTimeFieldSpecs,
complexFieldSpecs
+ Assert.assertFalse(jsonNode.has("metricFieldSpecs"),
+ "Empty metricFieldSpecs should be omitted");
+ Assert.assertFalse(jsonNode.has("dateTimeFieldSpecs"),
+ "Empty dateTimeFieldSpecs should be omitted");
+ Assert.assertFalse(jsonNode.has("complexFieldSpecs"),
+ "Empty complexFieldSpecs should be omitted");
+ Assert.assertFalse(jsonNode.has("timeFieldSpec"),
+ "Null timeFieldSpec should be omitted");
+ Assert.assertFalse(jsonNode.has("primaryKeyColumns"),
+ "Empty primaryKeyColumns should be omitted");
+ }
+
+ /**
+ * Tests that enableColumnBasedNullHandling is always included in
serialization.
+ */
+ @Test
+ public void
testJsonValueSerializationAlwaysIncludesEnableColumnBasedNullHandling()
+ throws Exception {
+ // Test with default value (false)
+ final Schema schemaWithDefault = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .addSingleValueDimension("dim1", FieldSpec.DataType.STRING)
+ .build();
+
+ final String jsonStringDefault =
JsonUtils.objectToString(schemaWithDefault);
+ final JsonNode jsonNodeDefault =
JsonUtils.stringToJsonNode(jsonStringDefault);
+
+ Assert.assertTrue(jsonNodeDefault.has("enableColumnBasedNullHandling"),
+ "enableColumnBasedNullHandling should always be present");
+
Assert.assertFalse(jsonNodeDefault.get("enableColumnBasedNullHandling").asBoolean());
+
+ // Test with non-default value (true)
+ final Schema schemaWithEnabled = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .setEnableColumnBasedNullHandling(true)
+ .addSingleValueDimension("dim1", FieldSpec.DataType.STRING)
+ .build();
+
+ final String jsonStringEnabled =
JsonUtils.objectToString(schemaWithEnabled);
+ final JsonNode jsonNodeEnabled =
JsonUtils.stringToJsonNode(jsonStringEnabled);
+
+ Assert.assertTrue(jsonNodeEnabled.has("enableColumnBasedNullHandling"));
+
Assert.assertTrue(jsonNodeEnabled.get("enableColumnBasedNullHandling").asBoolean());
+ }
+
+ /**
+ * Tests that ComplexFieldSpec with MAP type serializes correctly.
+ */
+ @Test
+ public void testJsonValueSerializationWithComplexFieldSpecMap()
+ throws Exception {
+ final ComplexFieldSpec mapField = new ComplexFieldSpec("mapField",
FieldSpec.DataType.MAP, true, Map.of());
+
+ final Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .addField(mapField)
+ .build();
+
+ final String jsonString = JsonUtils.objectToString(schema);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ // Should have complexFieldSpecs
+ Assert.assertTrue(jsonNode.has("complexFieldSpecs"));
+ final JsonNode complexSpecs = jsonNode.get("complexFieldSpecs");
+ Assert.assertEquals(complexSpecs.size(), 1);
+
+ final JsonNode mapSpec = complexSpecs.get(0);
+ Assert.assertEquals(mapSpec.get("name").asText(), "mapField");
+ Assert.assertEquals(mapSpec.get("dataType").asText(), "MAP");
+ Assert.assertEquals(mapSpec.get("fieldType").asText(), "COMPLEX");
+
+ // defaultNullValue should be omitted since empty Map is the default
+ Assert.assertFalse(mapSpec.has("defaultNullValue"),
+ "Empty Map default should not be serialized");
+
+ // Verify round-trip
+ final Schema deserializedSchema = Schema.fromString(jsonString);
+ Assert.assertNotNull(deserializedSchema.getFieldSpecFor("mapField"));
+
Assert.assertEquals(deserializedSchema.getFieldSpecFor("mapField").getDataType(),
FieldSpec.DataType.MAP);
+ }
+
+ /**
+ * Tests that ComplexFieldSpec with LIST type serializes correctly.
+ */
+ @Test
+ public void testJsonValueSerializationWithComplexFieldSpecList()
+ throws Exception {
+ final ComplexFieldSpec listField = new ComplexFieldSpec("listField",
FieldSpec.DataType.LIST, true, Map.of());
+
+ final Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .addField(listField)
+ .build();
+
+ final String jsonString = JsonUtils.objectToString(schema);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ // Should have complexFieldSpecs
+ Assert.assertTrue(jsonNode.has("complexFieldSpecs"));
+ final JsonNode complexSpecs = jsonNode.get("complexFieldSpecs");
+ Assert.assertEquals(complexSpecs.size(), 1);
+
+ final JsonNode listSpec = complexSpecs.get(0);
+ Assert.assertEquals(listSpec.get("name").asText(), "listField");
+ Assert.assertEquals(listSpec.get("dataType").asText(), "LIST");
+
+ // Verify round-trip
+ final Schema deserializedSchema = Schema.fromString(jsonString);
+ Assert.assertNotNull(deserializedSchema.getFieldSpecFor("listField"));
+
Assert.assertEquals(deserializedSchema.getFieldSpecFor("listField").getDataType(),
FieldSpec.DataType.LIST);
+ }
+
+ /**
+ * Tests that DateTimeFieldSpec serializes correctly with format and
granularity.
+ */
+ @Test
+ public void testJsonValueSerializationWithDateTimeFieldSpec()
+ throws Exception {
+ final Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .addDateTime("timestamp", FieldSpec.DataType.LONG,
"1:MILLISECONDS:EPOCH", "1:MILLISECONDS")
+ .build();
+
+ final String jsonString = JsonUtils.objectToString(schema);
+ final JsonNode jsonNode = JsonUtils.stringToJsonNode(jsonString);
+
+ Assert.assertTrue(jsonNode.has("dateTimeFieldSpecs"));
+ final JsonNode dateTimeSpecs = jsonNode.get("dateTimeFieldSpecs");
+ Assert.assertEquals(dateTimeSpecs.size(), 1);
+
+ final JsonNode dtSpec = dateTimeSpecs.get(0);
+ Assert.assertEquals(dtSpec.get("name").asText(), "timestamp");
+ Assert.assertEquals(dtSpec.get("dataType").asText(), "LONG");
+ Assert.assertEquals(dtSpec.get("format").asText(), "1:MILLISECONDS:EPOCH");
+ Assert.assertEquals(dtSpec.get("granularity").asText(), "1:MILLISECONDS");
+
+ // defaultNullValue should be omitted since Long.MIN_VALUE is the default
for DATE_TIME LONG
+ Assert.assertFalse(dtSpec.has("defaultNullValue"),
+ "Default null value should not be serialized for DATE_TIME LONG");
+ }
+
+ /**
+ * Tests that Jackson serialization output matches toJsonObject() output.
+ */
+ @Test
+ public void testJacksonSerializationMatchesToJsonObject()
+ throws Exception {
+ final Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .setEnableColumnBasedNullHandling(true)
+ .addSingleValueDimension("dim1", FieldSpec.DataType.STRING)
+ .addSingleValueDimension("dim2", FieldSpec.DataType.INT, 42)
+ .addMultiValueDimension("mvDim", FieldSpec.DataType.DOUBLE)
+ .addMetric("metric1", FieldSpec.DataType.LONG)
+ .addDateTime("ts", FieldSpec.DataType.LONG, "1:HOURS:EPOCH", "1:HOURS")
+ .setPrimaryKeyColumns(Lists.newArrayList("dim1"))
+ .build();
+
+ // Get JSON from Jackson serialization
+ final String jacksonJson = JsonUtils.objectToString(schema);
+ final JsonNode jacksonNode = JsonUtils.stringToJsonNode(jacksonJson);
+
+ // Get JSON from toJsonObject()
+ final JsonNode toJsonObjectNode = schema.toJsonObject();
+
+ // They should be equal
+ Assert.assertEquals(jacksonNode, toJsonObjectNode,
+ "Jackson serialization should match toJsonObject() output");
+ }
+
+ /**
+ * Tests round-trip serialization/deserialization with a complex schema.
+ */
+ @Test
+ public void testRoundTripSerializationWithComplexSchema()
+ throws Exception {
+ final Schema originalSchema = new Schema.SchemaBuilder()
+ .setSchemaName("complexTestSchema")
+ .setEnableColumnBasedNullHandling(true)
+ .addSingleValueDimension("stringDim", FieldSpec.DataType.STRING)
+ .addSingleValueDimension("intDimWithDefault", FieldSpec.DataType.INT,
100)
+ .addMultiValueDimension("mvStringDim", FieldSpec.DataType.STRING,
"default")
+ .addMetric("longMetric", FieldSpec.DataType.LONG)
+ .addMetric("doubleMetricWithDefault", FieldSpec.DataType.DOUBLE, 3.14)
+ .addDateTime("eventTime", FieldSpec.DataType.LONG,
"1:MILLISECONDS:EPOCH", "1:MILLISECONDS")
+ .addDateTime("dayTime", FieldSpec.DataType.STRING,
"1:DAYS:SIMPLE_DATE_FORMAT:yyyy-MM-dd", "1:DAYS")
+ .setPrimaryKeyColumns(Lists.newArrayList("stringDim", "eventTime"))
+ .build();
+
+ // Serialize with Jackson
+ final String jsonString = JsonUtils.objectToString(originalSchema);
+
+ // Deserialize
+ final Schema deserializedSchema = Schema.fromString(jsonString);
+
+ // Verify all fields
+ Assert.assertEquals(deserializedSchema.getSchemaName(),
"complexTestSchema");
+ Assert.assertTrue(deserializedSchema.isEnableColumnBasedNullHandling());
+
+ // Verify dimensions
+ Assert.assertNotNull(deserializedSchema.getDimensionSpec("stringDim"));
+
Assert.assertEquals(deserializedSchema.getDimensionSpec("intDimWithDefault").getDefaultNullValue(),
100);
+
Assert.assertFalse(deserializedSchema.getDimensionSpec("mvStringDim").isSingleValueField());
+
+ // Verify metrics
+ Assert.assertNotNull(deserializedSchema.getMetricSpec("longMetric"));
+
Assert.assertEquals(deserializedSchema.getMetricSpec("doubleMetricWithDefault").getDefaultNullValue(),
3.14);
+
+ // Verify date time
+ Assert.assertNotNull(deserializedSchema.getDateTimeSpec("eventTime"));
+
Assert.assertEquals(deserializedSchema.getDateTimeSpec("eventTime").getFormat(),
"1:MILLISECONDS:EPOCH");
+
+ // Verify primary keys
+ Assert.assertEquals(deserializedSchema.getPrimaryKeyColumns(),
Lists.newArrayList("stringDim", "eventTime"));
+ }
+
+ /**
+ * Tests that a fresh ObjectMapper produces the same serialization as
JsonUtils.
+ * This verifies that @JsonValue annotation works with any ObjectMapper, not
just JsonUtils.
+ */
+ @Test
+ public void testJsonValueWorksWithFreshObjectMapper()
+ throws Exception {
+ final Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testSchema")
+ .addSingleValueDimension("dim1", FieldSpec.DataType.STRING)
+ .addMetric("metric1", FieldSpec.DataType.LONG)
+ .build();
+
+ // Use a fresh ObjectMapper (simulates different Jackson configurations)
+ final ObjectMapper freshMapper = new ObjectMapper();
+ final String freshMapperJson = freshMapper.writeValueAsString(schema);
+
+ // Should still produce toJsonObject() format
+ Assert.assertFalse(freshMapperJson.contains("defaultNullValueString"),
+ "Fresh ObjectMapper should also use @JsonValue and omit
defaultNullValueString");
+
+ // Should be deserializable
+ final Schema deserializedSchema = Schema.fromString(freshMapperJson);
+ Assert.assertEquals(deserializedSchema.getSchemaName(), "testSchema");
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]