diff --git a/lang/c++/impl/Compiler.cc b/lang/c++/impl/Compiler.cc index c0b39f9e743..d69fd6548f4 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 e68eb365e2f..01596a50579 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 2aa39d4146e..13fe0121d27 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 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; }