From 5db72ab9d3b8e7989bccaca377d5e5a6e7da339c Mon Sep 17 00:00:00 2001
From: Makkar <124033019+hiteshmk05@users.noreply.github.com>
Date: Tue, 26 May 2026 22:28:46 +0530
Subject: [PATCH] add: C++26 annotations support for rename and skip (#2730)
---
include/simdjson/annotations.h | 63 ++++++++
.../simdjson/generic/builder/dependencies.h | 1 +
.../simdjson/generic/builder/json_builder.h | 36 +++--
.../simdjson/generic/ondemand/dependencies.h | 1 +
.../generic/ondemand/std_deserialize.h | 6 +-
tests/builder/CMakeLists.txt | 1 +
.../static_reflection_annotations_tests.cpp | 146 ++++++++++++++++++
7 files changed, 236 insertions(+), 18 deletions(-)
create mode 100644 include/simdjson/annotations.h
create mode 100644 tests/builder/static_reflection_annotations_tests.cpp
diff --git a/include/simdjson/annotations.h b/include/simdjson/annotations.h
new file mode 100644
index 000000000..1f6adef4d
--- /dev/null
+++ b/include/simdjson/annotations.h
@@ -0,0 +1,63 @@
+#ifndef SIMDJSON_ANNOTATIONS_H
+#define SIMDJSON_ANNOTATIONS_H
+
+#if SIMDJSON_STATIC_REFLECTION
+
+#include
+#include
+
+namespace simdjson {
+
+// Structural compile-time string — char array avoids the pointer-based
+// 'reflect_constant failed' that occurs with const char* / string_view members.
+template
+struct fixed_string {
+ char data[N];
+
+ consteval fixed_string(const char (&s)[N]) noexcept {
+ for (size_t i = 0; i < N; ++i) { data[i] = s[i]; }
+ }
+
+ consteval std::string_view view() const noexcept { return {data, N - 1}; }
+
+ consteval bool operator==(const fixed_string&) const noexcept = default;
+};
+
+// Usage: [[= simdjson::rename<"first_name">]] std::string firstName;
+namespace detail {
+ template
+ struct rename_t {
+ static constexpr auto name = Name;
+ };
+} // namespace detail
+
+template
+inline constexpr detail::rename_t rename{};
+
+// Usage: [[= simdjson::skip]] int internalCache;
+namespace detail {
+ struct skip_tag {};
+} // namespace detail
+
+inline constexpr detail::skip_tag skip{};
+
+// Returns the JSON key for a reflected data member.
+template
+consteval const char* get_json_key_name() {
+ template for (constexpr auto ann :
+ std::define_static_array(std::meta::annotations_of(dm))) {
+ constexpr auto ann_type = std::meta::type_of(ann);
+ if constexpr (std::meta::has_template_arguments(ann_type) &&
+ std::meta::template_of(ann_type) == ^^detail::rename_t) {
+ constexpr auto args =
+ std::define_static_array(std::meta::template_arguments_of(ann_type));
+ return std::define_static_string([:args[0]:].view());
+ }
+ }
+ return std::define_static_string(std::meta::identifier_of(dm));
+}
+
+} // namespace simdjson
+
+#endif // SIMDJSON_STATIC_REFLECTION
+#endif // SIMDJSON_ANNOTATIONS_H
\ No newline at end of file
diff --git a/include/simdjson/generic/builder/dependencies.h b/include/simdjson/generic/builder/dependencies.h
index a5a26abf6..e20509d8e 100644
--- a/include/simdjson/generic/builder/dependencies.h
+++ b/include/simdjson/generic/builder/dependencies.h
@@ -10,5 +10,6 @@
// Otherwise, amalgamation will fail.
#include "simdjson/concepts.h"
#include "simdjson/dom/fractured_json.h"
+#include "simdjson/annotations.h"
#endif // SIMDJSON_GENERIC_BUILDER_DEPENDENCIES_H
\ No newline at end of file
diff --git a/include/simdjson/generic/builder/json_builder.h b/include/simdjson/generic/builder/json_builder.h
index f3c8c8050..eefde5443 100644
--- a/include/simdjson/generic/builder/json_builder.h
+++ b/include/simdjson/generic/builder/json_builder.h
@@ -4,6 +4,7 @@
#define SIMDJSON_GENERIC_STRING_BUILDER_H
#include "simdjson/generic/builder/json_string_builder.h"
#include "simdjson/concepts.h"
+#include "simdjson/annotations.h"
#endif // SIMDJSON_CONDITIONAL_INCLUDE
#if SIMDJSON_STATIC_REFLECTION
@@ -246,26 +247,29 @@ simdjson_really_inline constexpr void atom(writer &w, const T &t) {
// through the writer's local pos. For arithmetic fields, the integer
// write happens directly via write_uint_jeaiii on w.ptr+w.pos, so pos
// never round-trips through memory.
- int i = 0;
+ bool first = true;
if (!w.ensure(1)) return;
w.ptr[w.pos++] = '{';
template for (constexpr auto dm : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
- constexpr auto first_key = std::define_static_string(
- constevalutil::consteval_to_quoted_escaped(std::meta::identifier_of(dm)) + ":");
- constexpr auto rest_key = std::define_static_string(
- std::string(",") + constevalutil::consteval_to_quoted_escaped(std::meta::identifier_of(dm)) + ":");
- constexpr size_t first_key_len = std::char_traits::length(first_key);
- constexpr size_t rest_key_len = std::char_traits::length(rest_key);
- if (!w.ensure(rest_key_len)) return;
- if (i == 0) {
- std::memcpy(w.ptr + w.pos, first_key, first_key_len);
- w.pos += first_key_len;
- } else {
- std::memcpy(w.ptr + w.pos, rest_key, rest_key_len);
- w.pos += rest_key_len;
+ if constexpr (std::meta::annotations_of_with_type(dm, ^^simdjson::detail::skip_tag).empty()) {
+ constexpr const char* key_name = simdjson::get_json_key_name();
+ constexpr auto first_key = std::define_static_string(
+ constevalutil::consteval_to_quoted_escaped(key_name) + ":");
+ constexpr auto rest_key = std::define_static_string(
+ std::string(",") + constevalutil::consteval_to_quoted_escaped(key_name) + ":");
+ constexpr size_t first_key_len = std::char_traits::length(first_key);
+ constexpr size_t rest_key_len = std::char_traits::length(rest_key);
+ if (!w.ensure(rest_key_len)) return;
+ if (first) {
+ std::memcpy(w.ptr + w.pos, first_key, first_key_len);
+ w.pos += first_key_len;
+ } else {
+ std::memcpy(w.ptr + w.pos, rest_key, rest_key_len);
+ w.pos += rest_key_len;
+ }
+ first = false;
+ atom(w, t.[:dm:]);
}
- atom(w, t.[:dm:]);
- i++;
};
if (!w.ensure(1)) return;
w.ptr[w.pos++] = '}';
diff --git a/include/simdjson/generic/ondemand/dependencies.h b/include/simdjson/generic/ondemand/dependencies.h
index 3c4fdc3a6..4914c20d8 100644
--- a/include/simdjson/generic/ondemand/dependencies.h
+++ b/include/simdjson/generic/ondemand/dependencies.h
@@ -14,5 +14,6 @@
#include "simdjson/padded_string_view.h"
#include "simdjson/internal/dom_parser_implementation.h"
#include "simdjson/jsonpathutil.h"
+#include "simdjson/annotations.h"
#endif // SIMDJSON_GENERIC_ONDEMAND_DEPENDENCIES_H
\ No newline at end of file
diff --git a/include/simdjson/generic/ondemand/std_deserialize.h b/include/simdjson/generic/ondemand/std_deserialize.h
index 36350ce85..38f5bc5ef 100644
--- a/include/simdjson/generic/ondemand/std_deserialize.h
+++ b/include/simdjson/generic/ondemand/std_deserialize.h
@@ -6,6 +6,7 @@
#include "simdjson/generic/ondemand/object.h"
#include "simdjson/generic/ondemand/array.h"
#include "simdjson/generic/ondemand/base.h"
+#include "simdjson/annotations.h"
#endif // SIMDJSON_CONDITIONAL_INCLUDE
#include
@@ -277,8 +278,9 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
SIMDJSON_TRY(val.get_object().get(obj));
}
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
- if constexpr (!std::meta::is_const(mem) && std::meta::is_public(mem)) {
- constexpr std::string_view key = std::define_static_string(std::meta::identifier_of(mem));
+ if constexpr (!std::meta::is_const(mem) && std::meta::is_public(mem)
+ && std::meta::annotations_of_with_type(mem, ^^simdjson::detail::skip_tag).empty()) {
+ constexpr std::string_view key = simdjson::get_json_key_name();
if constexpr (concepts::optional_type) {
// for optional members, it's ok if the key is missing
auto error = obj[key].get(out.[:mem:]);
diff --git a/tests/builder/CMakeLists.txt b/tests/builder/CMakeLists.txt
index df8501cb9..4f85ad44e 100644
--- a/tests/builder/CMakeLists.txt
+++ b/tests/builder/CMakeLists.txt
@@ -8,6 +8,7 @@ if(SIMDJSON_STATIC_REFLECTION)
add_cpp_test(static_reflection_edge_cases_tests LABELS ondemand acceptance per_implementation)
add_cpp_test(static_reflection_enum_tests LABELS ondemand acceptance per_implementation)
add_cpp_test(static_reflection_fractured_json_tests LABELS ondemand acceptance per_implementation)
+ add_cpp_test(static_reflection_annotations_tests LABELS ondemand acceptance per_implementation)
endif(SIMDJSON_STATIC_REFLECTION)
# Copy the simdjson dll into the tests directory
if(MSVC AND BUILD_SHARED_LIBS)
diff --git a/tests/builder/static_reflection_annotations_tests.cpp b/tests/builder/static_reflection_annotations_tests.cpp
new file mode 100644
index 000000000..65c91cdea
--- /dev/null
+++ b/tests/builder/static_reflection_annotations_tests.cpp
@@ -0,0 +1,146 @@
+#include "simdjson.h"
+#include "test_builder.h"
+#include
+#include
+
+using namespace simdjson;
+
+#if SIMDJSON_STATIC_REFLECTION
+
+struct RenamedFields {
+ [[= simdjson::rename<"first_name">]] std::string firstName = "";
+ [[= simdjson::rename<"last_name">]] std::string lastName = "";
+ int age = 0;
+};
+
+struct SkippedField {
+ std::string name = "";
+ [[= simdjson::skip]] int internalCache = 0;
+};
+
+struct MixedAnnotations {
+ [[= simdjson::rename<"user_name">]] std::string userName = "";
+ [[= simdjson::skip]] int sessionToken = 0;
+ int age = 0;
+};
+
+#endif // SIMDJSON_STATIC_REFLECTION
+
+namespace annotation_tests {
+
+bool rename_serialize_test() {
+ TEST_START();
+#if SIMDJSON_STATIC_REFLECTION
+ RenamedFields r{"Alice", "Smith", 30};
+ std::string out;
+ ASSERT_SUCCESS(simdjson::to_json(r).get(out));
+ ASSERT_EQUAL(out, "{\"first_name\":\"Alice\",\"last_name\":\"Smith\",\"age\":30}");
+#endif
+ TEST_SUCCEED();
+}
+
+bool rename_deserialize_test() {
+ TEST_START();
+#if SIMDJSON_STATIC_REFLECTION
+ std::string json = R"({"first_name":"Bob","last_name":"Jones","age":25})";
+ simdjson::ondemand::parser parser;
+ simdjson::ondemand::document doc;
+ ASSERT_SUCCESS(parser.iterate(simdjson::pad(json)).get(doc));
+ RenamedFields r;
+ ASSERT_SUCCESS(doc.get().get(r));
+ ASSERT_EQUAL(r.firstName, "Bob");
+ ASSERT_EQUAL(r.lastName, "Jones");
+ ASSERT_EQUAL(r.age, 25);
+#endif
+ TEST_SUCCEED();
+}
+
+bool rename_roundtrip_test() {
+ TEST_START();
+#if SIMDJSON_STATIC_REFLECTION
+ RenamedFields original{"Carol", "White", 40};
+ std::string json;
+ ASSERT_SUCCESS(simdjson::to_json(original).get(json));
+ simdjson::ondemand::parser parser;
+ simdjson::ondemand::document doc;
+ ASSERT_SUCCESS(parser.iterate(simdjson::pad(json)).get(doc));
+ RenamedFields result;
+ ASSERT_SUCCESS(doc.get().get(result));
+ ASSERT_EQUAL(result.firstName, original.firstName);
+ ASSERT_EQUAL(result.lastName, original.lastName);
+ ASSERT_EQUAL(result.age, original.age);
+#endif
+ TEST_SUCCEED();
+}
+
+bool skip_serialize_test() {
+ TEST_START();
+#if SIMDJSON_STATIC_REFLECTION
+ SkippedField s{"Alice", 999};
+ std::string out;
+ ASSERT_SUCCESS(simdjson::to_json(s).get(out));
+ ASSERT_EQUAL(out, "{\"name\":\"Alice\"}");
+#endif
+ TEST_SUCCEED();
+}
+
+bool skip_deserialize_ignores_field_test() {
+ TEST_START();
+#if SIMDJSON_STATIC_REFLECTION
+ // The key "internalCache" is present in JSON but the field is annotated skip -
+ // it should be ignored and the field should keep its default value.
+ std::string json = R"({"name":"Carol","internalCache":999})";
+ simdjson::ondemand::parser parser;
+ simdjson::ondemand::document doc;
+ ASSERT_SUCCESS(parser.iterate(simdjson::pad(json)).get(doc));
+ SkippedField s;
+ ASSERT_SUCCESS(doc.get().get(s));
+ ASSERT_EQUAL(s.name, "Carol");
+ ASSERT_EQUAL(s.internalCache, 0);
+#endif
+ TEST_SUCCEED();
+}
+
+bool mixed_annotations_serialize_test() {
+ TEST_START();
+#if SIMDJSON_STATIC_REFLECTION
+ MixedAnnotations m{"dave", 12345, 28};
+ std::string out;
+ ASSERT_SUCCESS(simdjson::to_json(m).get(out));
+ // sessionToken must not appear; userName must appear as "user_name"
+ ASSERT_EQUAL(out, "{\"user_name\":\"dave\",\"age\":28}");
+#endif
+ TEST_SUCCEED();
+}
+
+bool mixed_annotations_deserialize_test() {
+ TEST_START();
+#if SIMDJSON_STATIC_REFLECTION
+ std::string json = R"({"user_name":"eve","age":35})";
+ simdjson::ondemand::parser parser;
+ simdjson::ondemand::document doc;
+ ASSERT_SUCCESS(parser.iterate(simdjson::pad(json)).get(doc));
+ MixedAnnotations m;
+ ASSERT_SUCCESS(doc.get().get(m));
+ ASSERT_EQUAL(m.userName, "eve");
+ ASSERT_EQUAL(m.age, 35);
+ ASSERT_EQUAL(m.sessionToken, 0);
+#endif
+ TEST_SUCCEED();
+}
+
+bool run_all() {
+ return rename_serialize_test()
+ && rename_deserialize_test()
+ && rename_roundtrip_test()
+ && skip_serialize_test()
+ && skip_deserialize_ignores_field_test()
+ && mixed_annotations_serialize_test()
+ && mixed_annotations_deserialize_test();
+}
+
+} // namespace annotation_tests
+
+int main() {
+ return annotation_tests::run_all() ? EXIT_SUCCESS : EXIT_FAILURE;
+}