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]

Reply via email to