Repository: apache/avro
Pull request #3332: AVRO-4121: [C++] Fix get CustomAttributes from Node
Author: login: wgtmac
Labels: name: C++
description: Pull Requests for C++ binding
Created: 2025-03-05T06:26:58Z | Merged: 2025-03-17T17:33:36Z

## What is the purpose of the change

My previous work in https://github.com/apache/avro/pull/3325 introduces a new `Node::getCustomAttributes` API to return (consolidated) custom attributes. However, this implementation is wrong because a `Node` instance holds a separate CustomAttribute for each leaf node. This patch fixes it by replacing `Node::getCustomAttributes` with two new functions `Node::customAttributes` and `Node::customAttributesAt` to match other methods from Node for name, default value, etc.

## Verifying this change

This change added tests and can be verified as follows:

- Added test cases to SchemaTests to verify that custom attributes from different node types are preserved and parsed correctly.

## Documentation

- Does this pull request introduce a new feature? no
- If yes, how is the feature documented? not applicable



Diff:
diff --git a/lang/c++/include/avro/Node.hh b/lang/c++/include/avro/Node.hh
index ad01dddb475..7918c3302e7 100644
--- a/lang/c++/include/avro/Node.hh
+++ b/lang/c++/include/avro/Node.hh
@@ -170,7 +170,8 @@ public:
         doAddCustomAttribute(customAttributes);
     }
 
-    virtual CustomAttributes getCustomAttributes() const = 0;
+    virtual size_t customAttributes() const = 0;
+    virtual const CustomAttributes &customAttributesAt(size_t index) const = 0;
 
     virtual bool isValid() const = 0;
 
diff --git a/lang/c++/include/avro/NodeImpl.hh b/lang/c++/include/avro/NodeImpl.hh
index 4a35d6fb1ab..2038c9d5ce7 100644
--- a/lang/c++/include/avro/NodeImpl.hh
+++ b/lang/c++/include/avro/NodeImpl.hh
@@ -164,15 +164,16 @@ protected:
         customAttributes_.add(customAttributes);
     }
 
-    CustomAttributes getCustomAttributes() const override {
-        CustomAttributes mergedCustomAttributes;
-        for (size_t i = 0; i < customAttributes_.size(); i++) {
-            const auto &customAttribute = customAttributes_.get(i);
-            for (const auto &[key, value] : customAttribute.attributes()) {
-                mergedCustomAttributes.addAttribute(key, value);
-            }
+    size_t customAttributes() const override {
+        return customAttributes_.size();
+    }
+
+    const CustomAttributes &customAttributesAt(size_t index) const override {
+        if (index >= customAttributes_.size()) {
+            throw Exception("Custom attribute index {} is out of bounds for size {}",
+                            index, customAttributes_.size());
         }
-        return mergedCustomAttributes;
+        return customAttributes_.get(index);
     }
 
     SchemaResolution furtherResolution(const Node &reader) const {
diff --git a/lang/c++/test/SchemaTests.cc b/lang/c++/test/SchemaTests.cc
index 3593c682739..4830ef04cf3 100644
--- a/lang/c++/test/SchemaTests.cc
+++ b/lang/c++/test/SchemaTests.cc
@@ -18,6 +18,7 @@
 
 #include "Compiler.hh"
 #include "GenericDatum.hh"
+#include "NodeImpl.hh"
 #include "ValidSchema.hh"
 
 #include <boost/algorithm/string/replace.hpp>
@@ -695,6 +696,166 @@ static void testCustomLogicalType() {
     verifyCustomLogicalType(parsedSchema);
 }
 
+static void testParseCustomAttributes() {
+    const std::string schema = R"({
+        "type": "record",
+        "name": "my_record",
+        "fields": [
+            { "name": "long_field",
+              "type": ["null", "long"],
+              "field-id": 1 },
+            { "name": "array_field",
+              "type": { "type": "array", "items": "int", "element-id": 3 },
+              "field-id": 2,
+              "extra": "1", "extra2": "2" },
+            { "name": "map_field",
+              "type": { "type": "map", "values": "int", "key-id": 5, "value-id": 6 },
+              "field-id": 4,
+              "extra": "foo" },
+            { "name": "timestamp_field",
+              "type": "long", "logicalType": "timestamp-micros", "adjust-to-utc": true,
+              "field-id": 10,
+              "extra": "bar" },
+            { "name": "no_custom_attributes_field",
+              "type": "long" }
+        ]
+    })";
+
+    ValidSchema compiledSchema = compileJsonSchemaFromString(schema);
+    const NodePtr &root = compiledSchema.root();
+    BOOST_CHECK_EQUAL(root->customAttributes(), 5);
+
+    // long_field
+    {
+        auto customAttributes = root->customAttributesAt(0);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("field-id").value(), "1");
+    }
+
+    // array_field
+    {
+        auto customAttributes = root->customAttributesAt(1);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("extra").value(), "1");
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("extra2").value(), "2");
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("field-id").value(), "2");
+
+        auto arrayField = root->leafAt(1);
+        BOOST_CHECK_EQUAL(arrayField->customAttributes(), 1);
+        auto arrayFieldCustomAttributes = arrayField->customAttributesAt(0);
+        BOOST_CHECK_EQUAL(arrayFieldCustomAttributes.getAttribute("element-id").value(), "3");
+    }
+
+    // map_field
+    {
+        auto customAttributes = root->customAttributesAt(2);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("field-id").value(), "4");
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("extra").value(), "foo");
+
+        auto mapField = root->leafAt(2);
+        BOOST_CHECK_EQUAL(mapField->customAttributes(), 1);
+        auto mapFieldCustomAttributes = mapField->customAttributesAt(0);
+        BOOST_CHECK_EQUAL(mapFieldCustomAttributes.getAttribute("key-id").value(), "5");
+        BOOST_CHECK_EQUAL(mapFieldCustomAttributes.getAttribute("value-id").value(), "6");
+    }
+
+    // timestamp_field
+    {
+        auto customAttributes = root->customAttributesAt(3);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("field-id").value(), "10");
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("extra").value(), "bar");
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("adjust-to-utc").value(), "true");
+    }
+
+    // no_custom_attributes_field
+    {
+        auto customAttributes = root->customAttributesAt(4);
+        BOOST_CHECK_EQUAL(customAttributes.attributes().size(), 0);
+    }
+}
+
+static void testAddCustomAttributes() {
+    auto recordNode = std::make_shared<NodeRecord>();
+
+    // long_field
+    {
+        CustomAttributes customAttributes;
+        customAttributes.addAttribute("field-id", "1");
+        recordNode->addCustomAttributesForField(customAttributes);
+        recordNode->addLeaf(std::make_shared<NodePrimitive>(AVRO_LONG));
+        recordNode->addName("long_field");
+    }
+
+    // array_field
+    {
+        auto arrayField = std::make_shared<NodeArray>(SingleLeaf(std::make_shared<NodePrimitive>(AVRO_INT)));
+        CustomAttributes elementCustomAttributes;
+        elementCustomAttributes.addAttribute("element-id", "3");
+        arrayField->addCustomAttributesForField(elementCustomAttributes);
+
+        CustomAttributes customAttributes;
+        customAttributes.addAttribute("field-id", "2");
+        customAttributes.addAttribute("extra", "1");
+        customAttributes.addAttribute("extra2", "2");
+        recordNode->addCustomAttributesForField(customAttributes);
+        recordNode->addLeaf(arrayField);
+        recordNode->addName("array_field");
+    }
+
+    // map_field
+    {
+        auto mapField = std::make_shared<NodeMap>(SingleLeaf(std::make_shared<NodePrimitive>(AVRO_INT)));
+        CustomAttributes keyValueCustomAttributes;
+        keyValueCustomAttributes.addAttribute("key-id", "5");
+        keyValueCustomAttributes.addAttribute("value-id", "6");
+        mapField->addCustomAttributesForField(keyValueCustomAttributes);
+
+        CustomAttributes customAttributes;
+        customAttributes.addAttribute("field-id", "4");
+        customAttributes.addAttribute("extra", "foo");
+        recordNode->addCustomAttributesForField(customAttributes);
+        recordNode->addLeaf(mapField);
+        recordNode->addName("map_field");
+    }
+
+    // timestamp_field
+    {
+        auto timestampField = std::make_shared<NodePrimitive>(AVRO_LONG);
+        CustomAttributes customAttributes;
+        customAttributes.addAttribute("field-id", "10");
+        customAttributes.addAttribute("extra", "bar");
+        customAttributes.addAttribute("adjust-to-utc", "true");
+        recordNode->addCustomAttributesForField(customAttributes);
+        recordNode->addLeaf(timestampField);
+        recordNode->addName("timestamp_field");
+    }
+
+    const std::string expected = R"({
+        "type": "record",
+        "name": "",
+        "fields": [
+            { "name": "long_field",
+              "type": "long",
+              "field-id": "1" },
+            { "name": "array_field",
+              "type": { "type": "array", "items": "int", "element-id": "3" },
+              "extra": "1",
+              "extra2": "2",
+              "field-id": "2" },
+            { "name": "map_field",
+              "type": { "type": "map", "values": "int", "key-id": "5", "value-id": "6" },
+              "extra": "foo",
+              "field-id": "4" },
+            { "name": "timestamp_field",
+              "type": "long",
+              "adjust-to-utc": "true",
+              "extra": "bar",
+              "field-id": "10" }
+        ]
+    })";
+    ValidSchema schema(recordNode);
+    std::string json = schema.toJson();
+    BOOST_CHECK_EQUAL(removeWhitespaceFromSchema(json), removeWhitespaceFromSchema(expected));
+}
+
 } // namespace schema
 } // namespace avro
 
@@ -719,5 +880,7 @@ init_unit_test_suite(int /*argc*/, char * /*argv*/[]) {
                    avro::schema::malformedLogicalTypes);
     ts->add(BOOST_TEST_CASE(&avro::schema::testCompactSchemas));
     ts->add(BOOST_TEST_CASE(&avro::schema::testCustomLogicalType));
+    ts->add(BOOST_TEST_CASE(&avro::schema::testParseCustomAttributes));
+    ts->add(BOOST_TEST_CASE(&avro::schema::testAddCustomAttributes));
     return ts;
 }
diff --git a/lang/c++/test/unittest.cc b/lang/c++/test/unittest.cc
index feafa31a463..b0cb44c5b9f 100644
--- a/lang/c++/test/unittest.cc
+++ b/lang/c++/test/unittest.cc
@@ -1066,16 +1066,6 @@ void testNestedMapSchema() {
     BOOST_CHECK_EQUAL(expected, actual.str());
 }
 
-void testCustomAttributes() {
-    std::string schema = R"({"type":"array","items":"long","extra":"1","extra2":"2"})";
-    avro::ValidSchema compiledSchema =
-        compileJsonSchemaFromString(schema);
-    auto customAttributes = compiledSchema.root()->getCustomAttributes();
-    BOOST_CHECK_EQUAL(customAttributes.attributes().size(), 2);
-    BOOST_CHECK_EQUAL(customAttributes.getAttribute("extra").value(), "1");
-    BOOST_CHECK_EQUAL(customAttributes.getAttribute("extra2").value(), "2");
-}
-
 boost::unit_test::test_suite *
 init_unit_test_suite(int /*argc*/, char * /*argv*/[]) {
     using namespace boost::unit_test;
@@ -1096,7 +1086,6 @@ init_unit_test_suite(int /*argc*/, char * /*argv*/[]) {
                                     boost::make_shared<TestResolution>()));
     test->add(BOOST_TEST_CASE(&testNestedArraySchema));
     test->add(BOOST_TEST_CASE(&testNestedMapSchema));
-    test->add(BOOST_TEST_CASE(&testCustomAttributes));
 
     return test;
 }


Reviews:
user: login: thiru-mg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T00:10:02Z
user: login: wgtmac
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T01:23:22Z
user: login: martin-g
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T09:58:09Z
user: login: wgtmac
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T10:04:01Z
user: login: wgtmac
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T10:07:40Z
user: login: wgtmac
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T14:53:39Z
user: login: wgtmac
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T15:06:53Z
user: login: thiru-mg
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T17:33:25Z

Review comments:
diff_hunk: @@ -170,7 +170,8 @@ public:
         doAddCustomAttribute(customAttributes);
     }
 
-    virtual CustomAttributes getCustomAttributes() const = 0;
path: lang/c++/include/avro/Node.hh
user: login: thiru-mg
body: Changing the return type an existing member function is a backward incompatible change and my break existing client code. I suggest that we leave the existing function as such and name the new function something like `customAttributeCount()`.
created_at: 2025-03-06T00:09:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -170,7 +170,8 @@ public:
         doAddCustomAttribute(customAttributes);
     }
 
-    virtual CustomAttributes getCustomAttributes() const = 0;
path: lang/c++/include/avro/Node.hh
user: login: wgtmac
body: It is not a breaking change because that member function was added last week: https://github.com/apache/avro/pull/3325
created_at: 2025-03-06T01:23:22Z
author_association: MEMBER
diff_hunk: @@ -164,15 +164,12 @@ protected:
         customAttributes_.add(customAttributes);
     }
 
-    CustomAttributes getCustomAttributes() const override {
-        CustomAttributes mergedCustomAttributes;
-        for (size_t i = 0; i < customAttributes_.size(); i++) {
-            const auto &customAttribute = customAttributes_.get(i);
-            for (const auto &[key, value] : customAttribute.attributes()) {
-                mergedCustomAttributes.addAttribute(key, value);
-            }
-        }
-        return mergedCustomAttributes;
+    size_t customAttributes() const override {
+        return customAttributes_.size();
+    }
+
+    const CustomAttributes &customAttributesAt(size_t index) const override {
+        return customAttributes_.get(index);
path: lang/c++/include/avro/NodeImpl.hh
user: login: martin-g
body: What if the index is out of bounds ?
created_at: 2025-03-13T09:46:40Z
author_association: MEMBER
diff_hunk: @@ -658,6 +659,158 @@ static void testMalformedLogicalTypes(const char *schema) {
     BOOST_CHECK(datum.logicalType().type() == LogicalType::NONE);
 }
 
+static void testParseCustomAttributes() {
+    const std::string schema = R"({
+        "type": "record",
+        "name": "my_record",
+        "fields": [
+            { "name": "long_field",
+              "type": ["null", "long"],
+              "field-id": 1 },
+            { "name": "array_field",
+              "type": { "type": "array", "items": "int", "element-id": 3 },
+              "field-id": 2,
+              "extra": "1", "extra2": "2" },
+            { "name": "map_field",
+              "type": { "type": "map", "values": "int", "key-id": 5, "value-id": 6 },
+              "field-id": 4,
+              "extra": "foo" },
+            { "name": "timestamp_field",
+              "type": "long", "logicalType": "timestamp-micros", "adjust-to-utc": true,
+              "field-id": 10,
+              "extra": "bar" }
+        ]
+    })";
+
+    ValidSchema compiledSchema = compileJsonSchemaFromString(schema);
+    const NodePtr &root = compiledSchema.root();
+    BOOST_CHECK_EQUAL(root->customAttributes(), 4);
path: lang/c++/test/SchemaTests.cc
user: login: martin-g
body: Please add more random examples. For example a field without custom fields. I wonder what would be the value of `root->customAttributes()` then
created_at: 2025-03-13T09:51:33Z
author_association: MEMBER
diff_hunk: @@ -658,6 +659,158 @@ static void testMalformedLogicalTypes(const char *schema) {
     BOOST_CHECK(datum.logicalType().type() == LogicalType::NONE);
 }
 
+static void testParseCustomAttributes() {
+    const std::string schema = R"({
+        "type": "record",
+        "name": "my_record",
+        "fields": [
+            { "name": "long_field",
+              "type": ["null", "long"],
+              "field-id": 1 },
+            { "name": "array_field",
+              "type": { "type": "array", "items": "int", "element-id": 3 },
+              "field-id": 2,
+              "extra": "1", "extra2": "2" },
+            { "name": "map_field",
+              "type": { "type": "map", "values": "int", "key-id": 5, "value-id": 6 },
+              "field-id": 4,
+              "extra": "foo" },
+            { "name": "timestamp_field",
+              "type": "long", "logicalType": "timestamp-micros", "adjust-to-utc": true,
+              "field-id": 10,
+              "extra": "bar" }
+        ]
+    })";
+
+    ValidSchema compiledSchema = compileJsonSchemaFromString(schema);
+    const NodePtr &root = compiledSchema.root();
+    BOOST_CHECK_EQUAL(root->customAttributes(), 4);
+
+    // long_field
+    {
+        auto customAttributes = root->customAttributesAt(0);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("field-id").value(), "1");
+    }
+
+    // array_field
+    {
+        auto customAttributes = root->customAttributesAt(1);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("extra").value(), "1");
path: lang/c++/test/SchemaTests.cc
user: login: martin-g
body: I find this API weird.
So, if the custom attributes are at the field's root level then you read them with `root->customAttributesAt(1).getAttribute("extra")`. But if there is there is something custom in the `type` then you need https://github.com/apache/avro/pull/3332/files#diff-559b3bdb2519c3e90855b7755c68a914b6507152dd6d1efe4fa77783ad4502c4R702-R705

I don't know what should be the correct behavior though. I think the Rust SDK just ignores the custom attributes in `type: {...}`
created_at: 2025-03-13T09:56:19Z
author_association: MEMBER
diff_hunk: @@ -658,6 +659,158 @@ static void testMalformedLogicalTypes(const char *schema) {
     BOOST_CHECK(datum.logicalType().type() == LogicalType::NONE);
 }
 
+static void testParseCustomAttributes() {
+    const std::string schema = R"({
+        "type": "record",
+        "name": "my_record",
+        "fields": [
+            { "name": "long_field",
+              "type": ["null", "long"],
+              "field-id": 1 },
+            { "name": "array_field",
+              "type": { "type": "array", "items": "int", "element-id": 3 },
+              "field-id": 2,
+              "extra": "1", "extra2": "2" },
+            { "name": "map_field",
+              "type": { "type": "map", "values": "int", "key-id": 5, "value-id": 6 },
+              "field-id": 4,
+              "extra": "foo" },
+            { "name": "timestamp_field",
+              "type": "long", "logicalType": "timestamp-micros", "adjust-to-utc": true,
+              "field-id": 10,
+              "extra": "bar" }
+        ]
+    })";
+
+    ValidSchema compiledSchema = compileJsonSchemaFromString(schema);
+    const NodePtr &root = compiledSchema.root();
+    BOOST_CHECK_EQUAL(root->customAttributes(), 4);
+
+    // long_field
+    {
+        auto customAttributes = root->customAttributesAt(0);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("field-id").value(), "1");
+    }
+
+    // array_field
+    {
+        auto customAttributes = root->customAttributesAt(1);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("extra").value(), "1");
path: lang/c++/test/SchemaTests.cc
user: login: wgtmac
body: This API aligns with its friends like `names` and `leaves` because they are for sub-fields: https://github.com/apache/avro/blob/main/lang/c%2B%2B/include/avro/Node.hh#L143-L160
created_at: 2025-03-13T10:04:00Z
author_association: MEMBER
diff_hunk: @@ -164,15 +164,12 @@ protected:
         customAttributes_.add(customAttributes);
     }
 
-    CustomAttributes getCustomAttributes() const override {
-        CustomAttributes mergedCustomAttributes;
-        for (size_t i = 0; i < customAttributes_.size(); i++) {
-            const auto &customAttribute = customAttributes_.get(i);
-            for (const auto &[key, value] : customAttribute.attributes()) {
-                mergedCustomAttributes.addAttribute(key, value);
-            }
-        }
-        return mergedCustomAttributes;
+    size_t customAttributes() const override {
+        return customAttributes_.size();
+    }
+
+    const CustomAttributes &customAttributesAt(size_t index) const override {
+        return customAttributes_.get(index);
path: lang/c++/include/avro/NodeImpl.hh
user: login: wgtmac
body: I can add bound check. Users are expected to call `customAttributes()` before calling `customAttributesAt(i)` just like `nameAt` and `leafAt`.
created_at: 2025-03-13T10:07:40Z
author_association: MEMBER
diff_hunk: @@ -658,6 +659,158 @@ static void testMalformedLogicalTypes(const char *schema) {
     BOOST_CHECK(datum.logicalType().type() == LogicalType::NONE);
 }
 
+static void testParseCustomAttributes() {
+    const std::string schema = R"({
+        "type": "record",
+        "name": "my_record",
+        "fields": [
+            { "name": "long_field",
+              "type": ["null", "long"],
+              "field-id": 1 },
+            { "name": "array_field",
+              "type": { "type": "array", "items": "int", "element-id": 3 },
+              "field-id": 2,
+              "extra": "1", "extra2": "2" },
+            { "name": "map_field",
+              "type": { "type": "map", "values": "int", "key-id": 5, "value-id": 6 },
+              "field-id": 4,
+              "extra": "foo" },
+            { "name": "timestamp_field",
+              "type": "long", "logicalType": "timestamp-micros", "adjust-to-utc": true,
+              "field-id": 10,
+              "extra": "bar" }
+        ]
+    })";
+
+    ValidSchema compiledSchema = compileJsonSchemaFromString(schema);
+    const NodePtr &root = compiledSchema.root();
+    BOOST_CHECK_EQUAL(root->customAttributes(), 4);
+
+    // long_field
+    {
+        auto customAttributes = root->customAttributesAt(0);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("field-id").value(), "1");
+    }
+
+    // array_field
+    {
+        auto customAttributes = root->customAttributesAt(1);
+        BOOST_CHECK_EQUAL(customAttributes.getAttribute("extra").value(), "1");
path: lang/c++/test/SchemaTests.cc
user: login: wgtmac
body: I agree that it is weird and this is different to the Java library where the custom attributes are directly accessed via the Node. However, the current implementation of `Node::addCustomAttributesForField()` already assigns one CustomAttribute to each leaf node. So we can only access the CustomAttribute of leaf node via Record/Map/Array's `customAttributesAt(i)` without breaking anything.
created_at: 2025-03-13T14:53:38Z
author_association: MEMBER
diff_hunk: @@ -658,6 +659,158 @@ static void testMalformedLogicalTypes(const char *schema) {
     BOOST_CHECK(datum.logicalType().type() == LogicalType::NONE);
 }
 
+static void testParseCustomAttributes() {
+    const std::string schema = R"({
+        "type": "record",
+        "name": "my_record",
+        "fields": [
+            { "name": "long_field",
+              "type": ["null", "long"],
+              "field-id": 1 },
+            { "name": "array_field",
+              "type": { "type": "array", "items": "int", "element-id": 3 },
+              "field-id": 2,
+              "extra": "1", "extra2": "2" },
+            { "name": "map_field",
+              "type": { "type": "map", "values": "int", "key-id": 5, "value-id": 6 },
+              "field-id": 4,
+              "extra": "foo" },
+            { "name": "timestamp_field",
+              "type": "long", "logicalType": "timestamp-micros", "adjust-to-utc": true,
+              "field-id": 10,
+              "extra": "bar" }
+        ]
+    })";
+
+    ValidSchema compiledSchema = compileJsonSchemaFromString(schema);
+    const NodePtr &root = compiledSchema.root();
+    BOOST_CHECK_EQUAL(root->customAttributes(), 4);
path: lang/c++/test/SchemaTests.cc
user: login: wgtmac
body: Added such case. Let me know what you think.
created_at: 2025-03-13T15:06:53Z
author_association: MEMBER
diff_hunk: @@ -164,15 +164,12 @@ protected:
         customAttributes_.add(customAttributes);
     }
 
-    CustomAttributes getCustomAttributes() const override {
-        CustomAttributes mergedCustomAttributes;
-        for (size_t i = 0; i < customAttributes_.size(); i++) {
-            const auto &customAttribute = customAttributes_.get(i);
-            for (const auto &[key, value] : customAttribute.attributes()) {
-                mergedCustomAttributes.addAttribute(key, value);
-            }
-        }
-        return mergedCustomAttributes;
+    size_t customAttributes() const override {
+        return customAttributes_.size();
+    }
+
+    const CustomAttributes &customAttributesAt(size_t index) const override {
+        return customAttributes_.get(index);
path: lang/c++/include/avro/NodeImpl.hh
user: login: thiru-mg
body: I agree with @wgtmac . There are other occasions in the code such unchecked behavior is found. E.g. `leafAt`.
created_at: 2025-03-17T17:33:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: wgtmac
created_at: 2025-03-05T06:28:51Z
body: @martin-g @Fokko @thiru-mg Could you help review this? I think my previous implementation is not correct and this should be good now.
author_association: MEMBER
user: login: wgtmac
created_at: 2025-03-11T11:36:51Z
body: @thiru-mg Gentle ping :)
author_association: MEMBER
user: login: wgtmac
created_at: 2025-03-13T07:41:14Z
body: @martin-g @Fokko Could you help review this as well? I think this is a very tiny fix (without breaking backward compatibility).
author_association: MEMBER
user: login: wgtmac
created_at: 2025-03-17T12:49:05Z
body: I think I have addressed all your comments. Would you mind taking a look again? @martin-g 
author_association: MEMBER
user: login: wgtmac
created_at: 2025-03-18T01:18:00Z
body: Thank you @thiru-mg @martin-g!
author_association: MEMBER