This is an automated email from the ASF dual-hosted git repository.

Fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro.git


The following commit(s) were added to refs/heads/main by this push:
     new 209a3735ec AVRO-4351: [C++] Preserve primitive custom attributes 
(#4001)
209a3735ec is described below

commit 209a3735ec330679790824da54b7558db55e4e7f
Author: Gang Wu <[email protected]>
AuthorDate: Mon Sep 21 20:53:46 2026 +0800

    AVRO-4351: [C++] Preserve primitive custom attributes (#4001)
    
    Preserve unknown properties when parsing and serializing primitive
    schema objects. This keeps extensions such as Iceberg's adjust-to-utc
    annotation intact across schema JSON round trips.
---
 lang/c++/impl/Compiler.cc    |  7 +++++++
 lang/c++/impl/NodeImpl.cc    |  9 ++++++++-
 lang/c++/test/SchemaTests.cc | 25 +++++++++++++++++++++++++
 3 files changed, 40 insertions(+), 1 deletion(-)

diff --git a/lang/c++/impl/Compiler.cc b/lang/c++/impl/Compiler.cc
index c0b39f9e74..d69fd6548f 100644
--- a/lang/c++/impl/Compiler.cc
+++ b/lang/c++/impl/Compiler.cc
@@ -552,6 +552,13 @@ static NodePtr makeNode(const Entity &e, const Object &m,
         result = makeMapNode(e, m, st, ns);
     } else {
         result = makePrimitive(type);
+        if (result) {
+            CustomAttributes customAttributes;
+            getCustomAttributes(m, customAttributes);
+            if (!customAttributes.attributes().empty()) {
+                result->addCustomAttributesForField(customAttributes);
+            }
+        }
     }
 
     if (result) {
diff --git a/lang/c++/impl/NodeImpl.cc b/lang/c++/impl/NodeImpl.cc
index e68eb365e2..01596a5057 100644
--- a/lang/c++/impl/NodeImpl.cc
+++ b/lang/c++/impl/NodeImpl.cc
@@ -220,8 +220,10 @@ NodeSymbolic::resolve(const Node &reader) const {
 
 void NodePrimitive::printJson(std::ostream &os, size_t depth) const {
     bool hasLogicalType = logicalType().type() != LogicalType::NONE;
+    bool hasCustomAttributes = customAttributes_.size() != 0;
+    bool printAsObject = hasLogicalType || hasCustomAttributes;
 
-    if (hasLogicalType) {
+    if (printAsObject) {
         os << "{\n"
            << indent(depth) << "\"type\": ";
     }
@@ -232,6 +234,11 @@ void NodePrimitive::printJson(std::ostream &os, size_t 
depth) const {
         os << ",\n"
            << indent(depth);
         logicalType().printJson(os);
+    }
+    for (size_t i = 0; i != customAttributes_.size(); ++i) {
+        printCustomAttributes(customAttributes_.get(i), depth, os);
+    }
+    if (printAsObject) {
         os << "\n}";
     }
     if (!getDoc().empty()) {
diff --git a/lang/c++/test/SchemaTests.cc b/lang/c++/test/SchemaTests.cc
index 2aa39d4146..13fe0121d2 100644
--- a/lang/c++/test/SchemaTests.cc
+++ b/lang/c++/test/SchemaTests.cc
@@ -890,6 +890,30 @@ static void testCustomAttributesJson2Schema2Json() {
     BOOST_CHECK_EQUAL(removeWhitespaceFromSchema(json), 
removeWhitespaceFromSchema(schema));
 }
 
+static void testPrimitiveCustomAttributesJsonRoundTrip() {
+    const std::vector<std::string> schemas = {
+        R"({
+            "type": "long",
+            "logicalType": "timestamp-micros",
+            "adjust-to-utc": true
+        })",
+        R"({
+            "type": "long",
+            "custom-property": "value"
+        })",
+    };
+
+    // Iceberg relies on adjust-to-utc to distinguish timestamp from 
timestamptz.
+    // Primitive schema properties must also round-trip without a logical type.
+    for (const auto &schema : schemas) {
+        ValidSchema compiledSchema = compileJsonSchemaFromString(schema);
+        BOOST_REQUIRE_EQUAL(compiledSchema.root()->customAttributes(), 1);
+
+        std::string json = compiledSchema.toJson();
+        BOOST_CHECK_EQUAL(removeWhitespaceFromSchema(json), 
removeWhitespaceFromSchema(schema));
+    }
+}
+
 static void testCustomAttributesSchema2Json2Schema() {
     const std::string expected = R"({
         "type": "record",
@@ -951,6 +975,7 @@ init_unit_test_suite(int /*argc*/, char * /*argv*/[]) {
     ts->add(BOOST_TEST_CASE(&avro::schema::testParseCustomAttributes));
     ts->add(BOOST_TEST_CASE(&avro::schema::testAddCustomAttributes));
     
ts->add(BOOST_TEST_CASE(&avro::schema::testCustomAttributesJson2Schema2Json));
+    
ts->add(BOOST_TEST_CASE(&avro::schema::testPrimitiveCustomAttributesJsonRoundTrip));
     
ts->add(BOOST_TEST_CASE(&avro::schema::testCustomAttributesSchema2Json2Schema));
     return ts;
 }

Reply via email to