Skip to content

feature: impl serde for cxx#17

Merged
16bit-ykiko merged 17 commits intomainfrom
cxx-serde
Feb 15, 2026
Merged

feature: impl serde for cxx#17
16bit-ykiko merged 17 commits intomainfrom
cxx-serde

Conversation

@16bit-ykiko
Copy link
Member

@16bit-ykiko 16bit-ykiko commented Feb 15, 2026

Summary by CodeRabbit

  • New Features

    • Compile-time reflection helpers and a reflectable-class constraint.
    • Comprehensive generic serialization API plus attribute utilities (alias/rename/skip/flatten).
    • SIMD-accelerated JSON serializer and deserializer.
    • Modular optional components (async, serde/simdjson, reflection, zest) exposed via build options.
  • Tests

    • New SIMD-JSON test suite covering vectors, maps, tuples, optionals, variants, annotated/reflectable structs, byte spans, and edge cases.
    • Conditional test wiring to include/exclude async and simdjson tests based on build options.
  • Chores

    • Unified CI/build workflows and configurable build options for async and simdjson.

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a serde subsystem (attrs, traits, generic serialize), SIMDJSON-backed serializer/deserializer and tests; extends reflection with field_count and reflectable_class; reorganizes CMake/xmake to expose new public targets (reflection, serde, async, zest) with conditional FetchContent for libuv/simdjson and updates test wiring and CI.

Changes

Cohort / File(s) Summary
Build / CMake / CI
CMakeLists.txt, .github/workflows/cmake.yml, .github/workflows/xmake.yml
Adds CMake options for async/test/zest/simdjson, new public targets (eventide::reflection, eventide::serde, eventide::async, eventide::zest), conditional FetchContent for libuv/simdjson, reworked unit_tests wiring, and unified CI/Xmake flows.
Reflection
include/reflection/struct.h
Adds field_count<Object>, refl::reflectable_class concept, makes some field accessors constexpr, and adjusts field_type resolution.
Serde — attributes
include/serde/attrs.h
New attribute system: annotate<T,Attrs...> (wrap/inherit variants), fixed_string<N>, serde::attr helpers (alias, flatten, literal, rename, skip, skip_if) and predicates in serde::pred.
Serde — core API
include/serde/serde.h
Adds serialize_traits forward declaration and a generic serialize(S&, const V&) -> std::expected dispatcher covering custom traits, primitives, optionals/variants, ranges, tuples, and reflectable structs.
Serde — traits & concepts
include/serde/traits.h
Introduces type/category concepts (bool/int/uint/str/bytes/etc.), utilities (dependent_false, is_specialization_of), range_format/format_kind, and comprehensive serializer_like / deserializer_like concepts.
SIMDJSON serializer
include/serde/simdjson/serializer.h
New serde::json::simd::Serializer implementing serializer_like: result/error types, SerializeArray/SerializeObject, full serialize_* primitives, structural APIs, stack/state and error handling, output accessors.
SIMDJSON deserializer
include/serde/simdjson/deserializer.h
New serde::json::simd::Deserializer with array/object helpers, streaming-like iteration, scalar/sequence/map/struct deserializers, from_json helpers, and public error/result types.
Serde tests
tests/serde/simdjson_tests.cpp
Adds SIMDJSON-based tests exercising vectors, maps, tuples, optionals, variants, reflectable and annotated structs, byte spans, non-finite float behavior, and serializer-state isolation.
Zest / test utilities
include/zest/macro.h
Adds expected-aware helpers (is_expected, binary_equal) and updates test macros to use binary_equal for comparisons with std::expected.
Xmake integration
xmake.lua
Adds serde_simdjson option, conditional simdjson dependency/public package link, conditional test/source inclusion, and libuv version bump.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Caller
participant serde::serialize as "serde::serialize"
participant simd::Serializer as "serde::json::simd::Serializer"
participant sb as "simdjson string_builder"
Caller->>serde::serialize: serialize(value)
alt custom serialize_traits available
serde::serialize->>serialize_traits: call custom serializer
serialize_traits-->>serde::serialize: std::expected<out,err>
else generic path
serde::serialize->>simd::Serializer: call serialize_* / structural APIs
simd::Serializer->>sb: append/encode tokens (strings, numbers, nulls, structure)
sb-->>simd::Serializer: status / error
simd::Serializer-->>serde::serialize: std::expected<output,err>
end
serde::serialize-->>Caller: return result

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I count each field with whiskered pride,

I tuck attributes in burrows deep inside.
simdjson sings while CMake plants new lanes,
Tests hop through hoops and the rabbit refrains.
Cheerful bytes and fields — serialize, abide!

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'feature: impl serde for cxx' is vague and does not accurately represent the primary changes, which involve implementing a comprehensive serialization framework with reflection, async support, SIMDJSON integration, and test infrastructure. Replace with a more specific title such as 'feature: add modular serde serialization with reflection, async, and SIMDJSON support' or 'feature: implement serde serialization framework with reflection and SIMDJSON'.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cxx-serde

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@CMakeLists.txt`:
- Around line 127-134: The TEST_SOURCES file(GLOB ...) call doesn't recurse so
nested test .cpp files are missed; update the CMake logic that defines
TEST_SOURCES (the file(GLOB TEST_SOURCES ...) invocation) to use
file(GLOB_RECURSE TEST_SOURCES ...) instead, preserving the CONFIGURE_DEPENDS
flag and the subsequent list(FILTER TEST_SOURCES ...) and message() behavior so
the EVENTIDE_ENABLE_ASYNC exclusion still applies.
- Around line 82-85: The file(GLOB call defining EVENTIDE_SOURCES uses a
redundant "**" pattern and should be replaced with a recursive glob; remove the
second pattern ("${CMAKE_CURRENT_SOURCE_DIR}/src/eventide/**/*.cpp") and change
the invocation to use file(GLOB_RECURSE EVENTIDE_SOURCES CONFIGURE_DEPENDS ...)
so that EVENTIDE_SOURCES is populated recursively and the intent is explicit
(update the file(GLOB EVENTIDE_SOURCES ...) invocation accordingly).

In `@include/serde/attrs.h`:
- Around line 11-12: Rename the concept identifier from warp_type to wrap_type
in the template declaration and update all references to it in this header (the
template declaration "template <typename T> concept warp_type = ..." and every
use of warp_type elsewhere in include/serde/attrs.h) so the name matches the
intended "wrap" semantics; ensure the concept signature remains unchanged
(template <typename T> concept wrap_type = !std::is_class_v<T> ||
std::is_final_v<T>), and update any comments or identifiers that referenced
warp_type to use wrap_type.

In `@include/serde/serde.h`:
- Line 42: Replace the range-for loop element type from auto& to auto&& wherever
you iterate over the container/view named v (and the other occurrence around
line 56) so the loop can bind to prvalues and proxy iterator returns; update the
loop header "for(auto& e: v)" to "for(auto&& e: v)" in the relevant loops (e.g.,
the for loop in serde.h that currently reads for(auto& e: v)) to handle all
value categories.
- Around line 22-33: The dispatch chain in the serializer overload is missing a
branch for floating_like, so floating-point values fall through to the final
static_assert; add an else if constexpr(floating_like<V>) branch that calls
s.serialize_float(v) (matching the serializer_like requirement) near the other
type-dispatch branches (the same sequence that currently checks bool_like,
int_like, uint_like, char_like, str_like, bytes_like) so serialize_float is used
for float/double/long double types and the static_assert is no longer triggered
for floating types.
- Around line 36-51: The code assumes std::ranges::size(v) for all ranges but
that requires sized_range; change both branches to pass an optional size to
serialize_seq/serialize_map by testing std::ranges::sized_range<decltype(v)> (or
concept sized_range) and only calling std::ranges::size(v) when true, otherwise
pass std::nullopt; update the sequence branch (serialize_seq, s_seq, element
loop) and apply the identical pattern to the map branch (serialize_map, s_map)
so both accept an optional<std::size_t> size parameter when available.

In `@include/serde/traits.h`:
- Around line 62-63: The file defines the concept bytes_like using
std::span<const std::byte> but does not include the header that provides
std::span; add the missing include directive to this translation unit (include
<span>) so bytes_like and std::span<const std::byte> are defined portably;
update/include the header near other standard includes in the file where
bytes_like is declared.
- Around line 144-145: The serializer_like concept incorrectly tests
serialize_bytes with a std::string_view; update the requires clause for
serializer_like to also declare a std::span<const std::byte> (or replace the
string_view use) and call s.serialize_bytes(byte_span) so the concept matches
bytes_like and real callers; specifically modify the serializer_like requires
that references serialize_bytes to accept a std::span<const std::byte> (matching
bytes_like) while keeping serialize_str tests with std::string_view.
🧹 Nitpick comments (5)
include/serde/attrs.h (2)

54-78: fixed_string(const char*) constructor has no bounds safety.

The const char* constructor at line 56 copies exactly N characters from str with no way to verify the pointer addresses at least N valid bytes. This is fine in constexpr contexts (UB would be caught at compile time), but if ever evaluated at runtime it's a silent buffer over-read.

Consider adding a static_assert or a runtime check, or removing this overload in favor of the array-reference constructor (which at least carries size info).


25-39: Consider adding constexpr to conversion operators for compile-time use.

The annotate<warp_type T, ...> specialization's conversion operators (lines 29, 33) are not marked constexpr. Given that the rest of the framework is heavily constexpr-oriented, this limits compile-time usability of annotated values.

Proposed fix
-    operator T&() {
+    constexpr operator T&() {
         return value;
     }
 
-    operator const T&() const {
+    constexpr operator const T&() const {
         return value;
     }
include/serde/serde.h (1)

15-16: Minor style inconsistency in typename usage for dependent types.

Line 15 uses typename T = typename S::value_type (explicit typename for the dependent name), while line 16 uses typename E = S::error_type (omitting the inner typename). Both are valid in C++20 (per P0634R3), but the inconsistency is distracting.

Proposed fix — pick one style
 template <serializer_like S,
           typename V,
-          typename T = typename S::value_type,
-          typename E = S::error_type>
+          typename T = S::value_type,
+          typename E = S::error_type>
CMakeLists.txt (2)

54-54: GIT_TAG v1.x tracks a branch — builds are non-reproducible.

Pinning to a branch means every fetch can pull different code. Consider pinning to a specific release tag or commit hash for reproducible builds.


141-144: Redundant cpptrace::cpptrace linkage.

eventide::zest already links cpptrace::cpptrace as PUBLIC (line 122), so it transitively propagates to unit_tests. The explicit link here is unnecessary.

Proposed fix
     target_link_libraries(unit_tests PRIVATE
         eventide::zest
-        cpptrace::cpptrace
     )

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@CMakeLists.txt`:
- Line 13: The CMake option name contains a typo: rename
option(EVENTIDE_SERDE_ENABLE_SIMEJSON ...) to
option(EVENTIDE_SERDE_ENABLE_SIMDJSON ...) and update all references accordingly
(including the preprocessor check and the `#error` in serializer.h that currently
references EVENTIDE_SERDE_ENABLE_SIMEJSON) so the option and macro use
EVENTIDE_SERDE_ENABLE_SIMDJSON consistently; search for
EVENTIDE_SERDE_ENABLE_SIMEJSON and replace with EVENTIDE_SERDE_ENABLE_SIMDJSON
in CMakeLists and serializer.h (and any other files) to fix the CI failure.
- Around line 164-172: The test glob TEST_SOURCES currently picks up all tests
including tests/serde/simdjson_tests.cpp which fails when
EVENTIDE_SERDE_ENABLE_SIMDJSON is OFF; add a conditional that checks
EVENTIDE_SERDE_ENABLE_SIMDJSON and, when OFF, run list(FILTER TEST_SOURCES
EXCLUDE REGEX ".*/tests/serde/.*simdjson.*" ) (or a suitable pattern to exclude
simdjson-related tests) and emit a message similar to the async block (e.g.,
message(STATUS "EVENTIDE_SERDE_ENABLE_SIMDJSON=OFF: skipping simdjson tests")).
Ensure you reference and modify the same TEST_SOURCES list and use the
EVENTIDE_SERDE_ENABLE_SIMDJSON variable.
- Around line 75-80: The FetchContent_Declare call currently pins simdjson to
the mutable branch by using GIT_TAG master which makes builds non-reproducible;
change the GIT_TAG value in the FetchContent_Declare(simdjson ...) block to a
specific tested release tag (e.g., v3.12.3) that is compatible with
simdjson::builder::string_builder, so the project always fetches that exact
release instead of the latest master; update any README or build docs to note
the pinned version and run a clean build to verify compatibility.

In `@include/serde/simdjson/serializer.h`:
- Around line 18-22: The `#error` message in serializer.h contains a typo:
"SIMEJSON" instead of "SIMDJSON" and references the misspelled CMake option
EVENTIDE_SERDE_ENABLE_SIMEJSON; update the string in the `#error` directive to
reference EVENTIDE_SERDE_ENABLE_SIMDJSON (and correct the human-readable
"SIMEJSON"→"SIMDJSON") so the macro name matches the intended option, and also
fix the corresponding misspelled CMake option in CMakeLists.txt so both the
`#error` message and CMake option use EVENTIDE_SERDE_ENABLE_SIMDJSON.
🧹 Nitpick comments (4)
include/serde/simdjson/serializer.h (2)

365-402: map_key_to_string — comprehensive but std::to_string for floats may produce surprising results.

std::to_string(double) uses sprintf-style formatting which can produce trailing zeros (e.g., "1.500000") rather than the compact representation you might expect. This is a minor ergonomic issue for float-keyed maps (which are uncommon). Consider whether std::format or snprintf with %g would be preferable.


462-468: Private member declarations — consider reserving the stack vector.

If typical serialization depth is known (e.g., 8–16 levels), a small initial reservation or using a small-buffer-optimized container could avoid repeated heap allocations for deeply nested structures. This is optional and only matters for performance-sensitive paths.

CMakeLists.txt (2)

88-95: Pinning libuv to v1.x branch has the same reproducibility concern.

Consider pinning to a specific release tag (e.g., v1.50.0) for deterministic builds.


96-99: CMAKE_BUILD_TYPE check doesn't work with multi-config generators.

CMAKE_BUILD_TYPE is empty for multi-config generators (Visual Studio, Ninja Multi-Config). The ASan flag for libuv will never activate under those generators. Since MSVC builds are already excluded by the NOT WIN32 guard, this currently only affects Linux multi-config setups — low risk but worth noting.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@include/serde/serde.h`:
- Around line 34-39: The optional-serialization branch in
is_specialization_of<std::optional, V> is inverted: when v.has_value() is true
it incorrectly calls s.serialize_none() and when false it calls
s.serialize_some(v.value()) (undefined behavior). Swap the branches so that
if(v.has_value()) calls s.serialize_some(v.value()) and the else calls
s.serialize_none(), ensuring you never call v.value() on an empty optional;
update the logic inside the optional specialization accordingly.
- Around line 40-41: The branch is checking is_specialization_of<std::variant,
T> where T is S::value_type; change the check to
is_specialization_of<std::variant, V> so the branch matches the actual value
type V; update the condition in the serde code path that currently reads "else
if constexpr(is_specialization_of<std::variant, T>)" to use V instead and keep
the std::visit call using v and s.serialize_some(value) as-is.
🧹 Nitpick comments (1)
include/serde/simdjson/serializer.h (1)

36-55: SerializeArray and SerializeObject hold a reference to Serializer — be cautious with move/copy semantics.

These nested helper types store a raw reference to the parent Serializer. Since serialize_seq/serialize_map etc. return them by value inside a std::expected, the caller must ensure the Serializer outlives the returned helper. This is currently safe given the usage pattern in serde::serialize, but consider deleting copy/move constructors or documenting the lifetime requirement to prevent misuse.

Also applies to: 57-87

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@CMakeLists.txt`:
- Around line 88-95: libuv is pinned to a moving branch via GIT_TAG v1.x which
hurts reproducible builds; update the FetchContent_Declare call for the target
named libuv to pin a specific release (use v1.51.0 to match xmake) by replacing
the GIT_TAG value with v1.51.0 while keeping other fetch options (e.g.,
GIT_SHALLOW TRUE) intact so CMake fetches a fixed commit instead of the moving
branch.
🧹 Nitpick comments (1)
xmake.lua (1)

76-79: Simdjson is added to the eventide (async) target — should it be on a separate serde target instead?

In CMakeLists.txt, simdjson is linked to eventide_serde (an interface library), not to the async library. Here in xmake, simdjson is added as a public dependency of the eventide target (which wraps async/libuv sources). This couples the async library to a serde dependency. Consider whether a separate serde target would be more appropriate for consistency with the CMake layout.

@16bit-ykiko
Copy link
Member Author

@coderabbitai summary

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

✅ Actions performed

Summary regeneration triggered.

@16bit-ykiko
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@include/serde/simdjson/deserializer.h`:
- Line 192: The assignment to pending_key uses field.escaped_key(), which
returns raw JSON escape sequences and breaks field matching for keys with
escapes; replace that call with field.unescaped_key() so pending_key receives
the decoded key string (update the assignment where pending_key is set from
field.escaped_key()).
- Around line 591-594: In skip_value(simdjson::ondemand::value& value) replace
the call to value.raw_json_token() with value.raw_json() so the full nested JSON
structure is consumed; call raw_json(), check the returned
simdjson_result<std::string_view> for errors (and log/propagate if needed) and
then discard the string_view to complete skipping the value and keep the parser
state consistent.
🧹 Nitpick comments (2)
.github/workflows/cmake.yml (1)

53-67: Build and test steps are correctly split for Windows vs. non-Windows.

The --config Debug for Windows (multi-config generator) and the corresponding Debug\ subdirectory for the test executable are correct.

One optional suggestion: consider using ctest --test-dir build-cmake (and --build-config Debug on Windows) instead of invoking the test binary directly. This gives you CTest features like timeouts, test filtering, and structured output for free.

♻️ Optional: use ctest instead of direct executable invocation
       - name: Test
         if: runner.os != 'Windows'
-        run: ./build-cmake/unit_tests
+        run: ctest --test-dir build-cmake --output-on-failure
 
       - name: Test (windows)
         if: runner.os == 'Windows'
-        run: .\build-cmake\Debug\unit_tests.exe
+        run: ctest --test-dir build-cmake --build-config Debug --output-on-failure
tests/serde/simdjson_tests.cpp (1)

97-115: Consider adding deserialize_optional and deserialize_variant test cases.

There are serialize_optional and serialize_variant tests but no corresponding deserialization tests. Adding round-trip coverage for these types would strengthen confidence in the serde paths, especially the optional deserialize_none/deserialize_some flow and the variant's active-alternative deserialization behavior.

@16bit-ykiko 16bit-ykiko merged commit 47d09cf into main Feb 15, 2026
16 checks passed
@16bit-ykiko 16bit-ykiko deleted the cxx-serde branch February 15, 2026 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant