Repository navigation
Releases
v0.7.0
Events can travel over MQTT, and they can be written and read in the Protobuf event format.
ce::mqtt is a new binding (MQTT 5.0 structured and binary mode, MQTT 3.1.1 structured mode), and ce::protobuf_format is a new, hand-written event format that also works as structured mode in the HTTP, Kafka and NATS bindings.
Around the SDK, the coverage gate now counts branches, and the Go and Java SDKs check the Protobuf bytes and the MQTT messages the way they already check HTTP, Kafka and NATS.
This is a feature release (D-BUILD-5: before 1.0 a feature release bumps the minor version, as a break does).
The reason, as the owner ruled on 2026-10-27: it adds a new binding and a new format, each a capability of its own with its own requirements, entry points and interop suite, and D-BUILD-4 as it stood would have numbered it v0.6.2.
There are no breaking changes.
Everything below is an addition to ce::v4 or a change outside the library, and ce::v1 to ce::v4 keep every declaration and every documented behaviour they had at v0.6.1: python3 scripts/frozen_api_diff.py --base v0.6.1 finds no frozen declaration removed or changed (1,843 declarations compared).
The published to_message, from_message and from_payload return what they returned for every input, a message of the Protobuf content type included, and no namespace is added, so ce::v4 stays the current generation.
Added
-
An MQTT binding, structured mode (
binding/mqtt.hpp,ce::mqtt, CR-0008 slice 1).
An event travels as an MQTT 5.0 PUBLISH held in ace::message: the JSON event as the payload, the Content Type propertyapplication/cloudevents+jsonas the headercontent-type, and no User Property written or read.
ce::mqtt::to_message<Codec>(event, ce::content_mode::structured)andce::mqtt::from_message<Codec>(message, options)are the 5.0 pair, withdetect_content_mode;ce::mqtt::to_payload<Codec>(event)andfrom_payload<Codec>(payload_or_message, options)are the MQTT 3.1.1 pair, which has no properties, so the caller names the version by the function.
A message with no CloudEvents Content Type isnot_a_cloudevent, a malformed payload is the codec's error, a batch is refused withinvalid_argumentand so is a second header namedcontent-type, and names match byte for byte.
Binary mode is the next bullet.
The binding is header-only, names no MQTT client library and adds the targetce::binding_mqtt, the module exportce::v4::mqttand the fuzz targetfuzz_mqtt_message; the Go SDK reads and writes the same structured messages (sixteen recorded goldens, no broker, sdk-java has no MQTT binding).
It is an addition toce::v4:python3 scripts/frozen_api_diff.py --base v0.6.1finds no changed declaration.
Use:auto publish = ce::mqtt::to_message<codec>(event, ce::content_mode::structured); auto received = ce::mqtt::from_message<codec>(*publish);
-
MQTT binary mode, with sdk-go's
ce-prefix as an opt-in (ce::mqtt, CR-0008 slice 2).
ce::mqtt::to_message<Codec>(event, ce::content_mode::binary_mode)writes an MQTT 5.0 PUBLISH whose payload is the data, whose Content Type property isdatacontenttypeand whose User Properties are every other attribute and extension;from_message<Codec>reads it.
The names follow the binding text, unchanged (specversion,id), by default.
Name the policy for the form the Go SDK writes and requires:to_message<Codec, ce::mqtt::prefixed_names>(...)andfrom_message<Codec, ce::mqtt::prefixed_names>(...)usece-specversion,ce-id;unprefixed_namesis the default and the concept isname_policy.
A message with a CloudEvents Content Type is still structured; any other is binary when it carries thespecversionUser Property under the policy, andnot_a_cloudeventotherwise, where an MQTT binding limited to structured mode would answernot_a_cloudeventfor every such message.
A User Property that is not an attribute name under the policy is ignored; adatacontenttypeUser Property must equal the Content Type property; values are well-formed UTF-8 without NUL and at most 65,535 bytes.
The eight Go binary goldens recorded with slice 1 now decode underprefixed_names, arenot_a_cloudeventunder the default, and Go reads the binary messages this SDK writes under the policy.
fuzz_mqtt_messagecovers binary mode under both policies, and the module also exportsname_policy,unprefixed_namesandprefixed_names.
Use:auto for_go = ce::mqtt::to_message<codec, ce::mqtt::prefixed_names>(event, ce::content_mode::binary_mode); auto back = ce::mqtt::from_message<codec, ce::mqtt::prefixed_names>(*for_go);
-
The Protobuf event format:
ce::protobuf_format, targetce::format_protobuf.
Bytes to event and event to bytes for the CloudEvents v1.0.2 Protobuf format, one event or a batch, hand-written over the standard library andce::core: nolibprotobuf, noprotoc, no generated code and no codec.
encode,decode,encode_batchanddecode_batch;decode_as<Codec>, which parses a JSON payload on request and holds no document; anddecode_detailedandencode_proto_data, which carry the type URL of aproto_datapayload beside the event.
The field numbers are those ofcloudevents.proto, not the prose of the specification.
The writer follows the specification (text_datafor text, the attributes in ascending order of name) and the reader accepts all three data fields, so the bytes of the Go and Java SDKs decode here and the bytes written here decode there.
Decoding is strict, uses the existing error codes, and is bounded byprotobuf::decode_options(16 MiB per event, 1024 attributes per event, 100,000 events per batch, all adjustable and checked before an allocation); a libFuzzer target and the goldens of both SDKs and ofprotocback it.
A decodedtimeis in UTC with the fewest of 0, 3, 6 or 9 fraction digits, and a Timestamp outside the range ofce::timestampisinvalid_timestamp.
It is an addition toce::v4and changes no declaration of any generation (CR-0009, SWR-PROTO-0001 to SWR-PROTO-0018, SWR-PROTO-0024 to SWR-PROTO-0026); structured mode in the bindings is the next bullet.
Use:auto bytes = ce::protobuf_format::encode(event); auto back = ce::protobuf_format::decode(*bytes);
-
Structured Protobuf in the HTTP, Kafka and NATS bindings (CR-0009 slice 2).
ce::http::,ce::kafka::andce::nats::each getto_message_protobuf(event), which writes theapplication/cloudevents+protobufcontent type under the binding's own header spelling and the bytes ofprotobuf_format::encode, andfrom_message_protobuf(message, options), which returnsnot_a_cloudeventfor a message whose media type, compared ignoring case and parameters, is another one.
NATS also gets the payload form,to_payload_protobuf(event)andfrom_payload_protobuf(bytes, options), which always reads Protobuf.
The functions are new names and not templates, there is no content mode and no moving variant, Kafka sets no key, and the publishedto_message,from_messageandfrom_payloadreturn what they returned for every input, a message of the Protobuf content type included.
The Go and Java SDKs' HTTP and Kafka bindings write and read the same messages (thirteen events each, no server and no broker), and the fuzz target for the decoder drives the new functions.
It is an addition toce::v4(SWR-PROTO-0019 to SWR-PROTO-0023).
Use:auto record = ce::kafka::to_message_protobuf(event); auto back = ce::kafka::from_message_protobuf(*record);
Tooling
None of this changes what the library does.
- Branch coverage is gated (CR-0007,
SWR-SEC-0007, D-SEC-5): thecoverage floorjob fails a pull request whose branch coverage, with the compiler's exception edges excluded, falls belowCE_COVERAGE_BRANCH_FLOOR(86), next to the unchanged 90% line floor.
The floor is valid for one toolchain, so the job asserts gcovr 7.0 and gcov-13 and fails before measuring, naming both versions, if the image ships others.
scripts/check_coverage_gate.pyand its cases (coverage_gate_toolsunder ctest) fail on a dropped flag, a literal floor or a floating toolchain.
The floor is raised by a person, in the pull request that raises the measurement by a whole point, and never lowered to let a release go out.
Tests were added for the untested error paths of JSON decode, header values, typed payloads and the payload scanner (#110). - The versioning rule (D-BUILD-5,
docs/RELEASE.md): before 1.0 a break and a feature release each bump the minor version, any other release the patch; the owner decides that a release is a feature release and the release pull request states the reason. - The
delivered_incheck is part of the spec gate (SWR-BUILD-0016, D-SPEC-5): every pull request runsderive_delivered_in.py --checkwith the full history, so a requirement that a tag delivered cannot stayapproved.
The gate is red between a tag and the merge of the post-tag pull request, by design, so that pull request is merged first. - The vcpkg port is proved against a release (
vcpkg-releasedworkflow,scripts/check_vcpkg_release_guard.py,SWR-ADOPT-0009): vcpkg downloads the archive of the tag withoutCLOUDEVENTS_CPP_SOURCE_DIR, refuses a hash that does not match, and the job also checks that a wrong hash is refused.
It skips, with a notice, while the tag is missing or the hash is still the previous release's. - Interop covers MQTT and Protobuf (
interop/mqtt,interop/protobuf,SWR-MQTT-0014,SWR-PROTO-0016,SWR-PROTO-0023, D-INTEROP-6): theinteropjob verifies the Go SDK's MQTT messages and the Go and Java SDKs' Protobuf bytes and HTTP and Kafka messages in both directions, without a broker or a server. - Two fuzz targets,
fuzz_mqtt_messageandfuzz_protobuf_format(whi...
v0.6.1
A forwarder can keep a binary-mode JSON body unparsed, and a receiver that is done with its message can move a non-JSON binary-mode body instead of copying it.
Around the SDK, a gate now fails a pull request that changes a frozen declaration, delivered_in is derived from the tags, Kafka records are checked against the Go and Java SDKs, vcpkg and Conan recipes are built in CI, and an advisory mutation report runs on pull requests.
There are no breaking changes, so this is a patch release (D-BUILD-4: before 1.0 a break bumps the minor version, an addition or a fix the patch).
Everything below is an addition to ce::v4 or a change outside the library, and ce::v1 to ce::v4 keep every declaration and every documented behaviour they had at v0.6.0: python3 scripts/frozen_api_diff.py --base v0.6.0 finds no frozen declaration removed or changed.
Two additions have a consequence that the rule accepts (CR-0005, rule 4 of docs/SPEC.md section 3):
json::decode_optionsgains a trailing member, so its size grows from 8 to 16 bytes and a structured bindingauto [limit] = options;stops compiling.
Every call and every initializer written for v0.6.0 ({.retain_document_up_to = n},{n},{}) compiles and behaves as before.decode_optionsis shared withce::v3, so a v3 call accepts the member and ignores it.
The moving entry points are new names, so no existing call changes.
Added
-
json::decode_options{.parse_binary_body = false}keeps a binary-mode JSON body unparsed.
A receiver that never reads the payload, such as a forwarder or router, no longer pays to parse it:from_messageof the HTTP, Kafka and NATS bindings stores the body's exact bytes asjson_text, without parsing or validating them, whatever their length and the retention limit.
v0.6.0 parses that body on receive, which costs 15% to 54% more instructions (RapidJSON +15%, Boost.JSON +18%, Glaze +29%, nlohmann +54%) and 8 to 27 more allocations on the HTTP full-event path, by codec; measured by the perf job (x86-64, g++-14), the receive of the full event over HTTP costs 35.6k instructions and 10 allocations (675 bytes) with the option, against 41.5k to 55.7k instructions, 16 to 37 allocations and 1.1 to 2.2 KiB with the parse (RapidJSON -14%, Boost.JSON -16%, Glaze -24%, nlohmann -36% in instructions); Kafka and NATS show the same shape.
The member is a trailingboolthat defaults totrue, so every call and every initializer written before compiles and behaves as it did.
It applies to binary mode only: structured and batched decode,nats::from_payloadandjson_format::decodeaccept it and ignore it, as does everyce::v3entry point.
With it set the caller takes over validation: a body that is not JSON is accepted as it came.
Use:auto passed_on = ce::http::from_message<codec>(request, {.parse_binary_body = false});
It is an addition to
ce::v4, so it needs no new namespace (CR-0005, SWR-BIND-0011 to SWR-BIND-0013). -
Moving entry points:
from_message_movingtakes a non-JSON binary-mode body instead of copying it.
A receiver that is done with its message, such as a gateway or a forwarder of images, archives or protobuf, hands it over withstd::move, and the call adopts the body's buffer as the event'sce::binarypayload: one allocation and one copy of the body fewer.
Six new names, ince::v4only, each taking the message (or the payload) by rvalue reference and otherwise identical to its copying sibling:http::from_message_moving<Codec, Values>,kafka::from_message_moving<Codec>,nats::from_message_moving<Codec>,binding::read_body_moving<Codec>, and, for symmetry,http::from_batch_message_moving<Codec>andnats::from_payload_moving<Codec>(which takes astd::string&&).
A body is moved only when it is non-empty, binary-mode, and its media type is absent or not oneis_json_content_typeaccepts.
After a successful call that moved the body,message.body.size() == 0andmessage.body.capacity() == 0(the buffer is released), andheader_fieldsis unchanged; that is a permanent promise, not an implementation detail.
Every other body takes the copying path exactly as the copying entry point does and leaves the message byte for byte intact: a JSON body (parsed or not, whateverdecode_optionssay), a structured message, a batch, anatspayload string and an empty body, which keeps whatever capacity it had.
A failed call leaves the message unchanged for every body, so it can still go to a dead-letter queue: the body is taken only after the event has passed its final validation.
The copying entry points are unchanged and never modify their message, withstd::moveor without (SWR-BIND-0017).
A JSON body saves nothing from a move, because the parse reads the bytes and astd::stringcannot adopt astd::vector<std::byte>; for a forwarder of JSON events the opt-in above is the saving.
Use:auto received = ce::kafka::from_message_moving<codec>(std::move(record.value));Measured by the perf job (x86-64, g++-14, nlohmann as the codec argument, the receive alone) against the copying receive of the same body: a 64 KiB
application/octet-streambody costs 84% to 88% fewer instructions (HTTP 77.4k to 11.6k, Kafka 74.6k to 8.9k, NATS 77.9k to 12.3k), one allocation fewer of four and 99.9% fewer allocated bytes (65.6 KB to under 100 B).
A 256 B body costs about the same instructions (HTTP -0.7%, Kafka -0.9%, NATS +0.6%), against an upper bound of 5% to 8% estimated before the code existed: building the event without the payload and moving the media type out and back costs nearly what the 256 B copy did.
It still makes one allocation fewer of four and allocates 72% to 75% fewer bytes, so the saving is in memory and in large bodies, not in instructions at small sizes.
It is an addition toce::v4, andce::v3has none of the new names (CR-0005 stage 2,SWR-BIND-0014toSWR-BIND-0018,SWR-HTTP-0018,SWR-HTTP-0019,SWR-KAFKA-0010,SWR-NATS-0005,SWR-NATS-0006).
Tooling
None of this changes what the library does.
- A frozen-API gate (
scripts/frozen_api_diff.py, thefrozen-apiworkflow): clang parses every public header at the latestv*tag and in the tree, and a pull request that removes or changes a declaration ofce::v1toce::v4fails with the declaration named.
A declaration that moved between headers or a body that was fixed passes, and so does a defaulted member appended to an aggregate (D-BIND-3).
Exit 2 means it did not run, which is not a pass (SWR-BUILD-0015, D-BUILD-4).
docs/RELEASE.mdcarries the versioning rule it supports: a break goes to a new generation and bumps the minor version, an addition or a fix bumps the patch. delivered_inis derived from the release tags, not typed (spec/tools/derive_delivered_in.py,SWR-BUILD-0016, D-SPEC-4).
--writeruns once the tag exists, and--checkreports what it would change; 195 of 222 requirements readimplementedwith a tag, and the 27 changed or added since v0.6.0 stayapproveduntil the next tag holds them.- Kafka records are checked against sdk-go and sdk-java the way HTTP and NATS documents already were: both SDKs decode every record this one writes, in binary and structured mode, and this one decodes theirs, in the ctest suite and in the
interopjob (SWR-SEC-0008, D-INTEROP-5). - vcpkg and Conan recipes (
packaging/vcpkg/ports/cloudevents-cpp,packaging/conan), each built and tested againsttest/consumerin thepackagingworkflow.
The nlohmann codec is the default feature or option; RapidJSON is the other one CI builds, and Boost.JSON is declared and not built there.
Install from a clone withvcpkg install cloudevents-cpp --overlay-ports=...orconan create packaging/conan; neither is submitted to the vcpkg registry or to ConanCenter, and that is not decided.
No CMake change: the build already looked CTRE up withfind_packagebefore it fetched it, and the recipes make a missed lookup an error.
The consumer gained a round-trip program that runs the library through the installed package, and a check fails a release that leaves the vcpkg manifest on the old version (SWR-ADOPT-0006toSWR-ADOPT-0008, D-ADOPT-2). - An advisory mutation report (
scripts/mutation.sh, themutationworkflow): Mull mutates the lines a pull request changes and the report is commented on the pull request.
It never blocks a merge.
Tests were added for what it found surviving in the timestamp, theresult<T>polyfill and the v4 document. - The perf job measures forty-four operations, up from twenty-three: a non-JSON binary-mode receive (empty, 256 B and 64 KiB, over HTTP, Kafka and NATS), the same for a text body over HTTP, the receive that leaves a binary-mode body unparsed, and the moving receive.
Measured
- Coverage, from the
coverage floorjob onmainat48f0eb1, the merge of the last change before this release (run 39632669677; GCC 13, C++20, Debug): lines 96.2% (4456 of 4630), functions 91.9% (2143 of 2331), branches 60.9% (4958 of 8146), against a floor of 90% lines. - CI toolchains are those of v0.6.0: GCC 13 at C++20 and C++23, Clang 16 with libstdc++ 13, Clang 17 with libc++ 17 at C++23, AppleClang at C++20 and C++23, MSVC 19 (Windows 2022), a C++26 static-reflection job on GCC 15 (GCC 14 if 15 is unavailable; allowed to fail), plus the forced polyfill, exceptions disabled, no default codec, RapidJSON alone, Boost.JSON alone, every codec on Linux and macOS at C++20 and C++23, ASan with UBSan and TSan on Clang 17, the clang-tidy gate on Homebrew LLVM, install-and-consume, the examples, and interop with the Go and Java SDKs, now including Kafka records.
New since v0.6.0: thefrozen-apiworkflow (Clang 18 against the latest tag), thepackagingworkflow (g++-14; the vcpkg port and the Conan recipe, each in three v...
v0.6.0
ce::v4 opens: a json_document can move, a typed write refuses a media type that is not JSON, and a JSON body received in binary mode keeps the document it was parsed into.
Each of those changes behaviour that v0.5.0 documented, and a documented behaviour is part of the API, so they ship as a fourth API generation, ce::v4, which is the inline namespace.
ce::v3 stays what v0.5.0 published, declarations and behaviour both, and the opt-in header <cloudevents/v3_conversion.hpp> moves events across.
Breaking
Every change below is in ce::v4; ce::v3 behaves as v0.5.0 did.
-
ce::now names v4.ce::event,ce::json_document,ce::json_format,ce::http,ce::kafka,ce::natsandce::decode_asare the v4 entities.
To keep v0.5.0 code compiling unchanged, spell the v3 namespace and include thev3/copy of each header that has one (core.hpp,format/json_format.hpp,format/typed_payload.hppand the four binding headers):#include <cloudevents/v3/binding/http.hpp> using namespace ce::v3::literals; auto order = ce::v3::event::builder{.id = "A1"_id, .source = "/orders"_source, .type = "com.example.placed"_type}.build(); auto request = ce::v3::http::to_message<ce::v3::codec::nlohmann_codec>( *order, ce::v3::content_mode::binary_mode);
Code that names only shared entities, such as an attribute type,
message,json::decode_optionsor a codec, needs no change.
To exchange events with code that moved to v4, include<cloudevents/v3_conversion.hpp>and callce::v4::from_v3andce::v4::to_v3. -
Moving a
ce::v4::json_documentleaves it reading as JSON null.
In v0.5.0 a move copied the document, so the source still held the same DOM, andce::v3::json_documentstill does.
In v4 a move hands the DOM over without touching the reference count, and the moved-from document is a valid, never-empty document that reads as null:dump()returnsnull,get<Codec>()returnsnullptrfor every codec, and it compares equal only to another moved-from document.
v0.5.0 documented the copy, so this is a break, and it is why v4 exists.
Migrate: copy the document instead of moving it where you read the source afterwards, or stay once::v3. -
set_datakeeps a JSON media type and refuses any other.
In v3set_data<T, Codec>setdatacontenttypetoapplication/jsonwhatever the event declared.
In v4 it setsapplication/jsononly when the event declares none, keeps a media typeis_json_content_typeaccepts (text/jsonand+jsontypes included), and refuses any other witherrc::type_mismatchnamingdatacontenttype, leaving the event unchanged, asencode_asalready did.
set_dataandevent_of::set_datareturn[[nodiscard]] result<void>.
Migrate: check the result (if (auto stored = ce::set_data<T, Codec>(event, value); !stored) { ... }); if an event declares a non-JSON type that you now want replaced, callevent.set_data(ce::json_document::make<Codec>(ce::to_json_value<Codec>(value)), "application/json"_mediatype)yourself. -
event_of::with_datareturns[[nodiscard]] result<event_of>, because it can be refused for the same reason.
Migrate: unwrap it (view->data()). -
A JSON body received in binary mode is a
json_document.
In v3 the HTTP, Kafka and NATSfrom_messagestored a binary-mode body under a JSON media type as unparsedjson_text.
In v4from_message<Codec>parses it with the codec, under the rules of structured decode: a payload within the 16 KiB retention limit is ajson_document, one above it isjson_textholding the body's bytes exactly as received, and a non-empty body that is not valid JSON fails witherrc::parse_error.
An empty body is still no payload, whatever its media type, and other media types are stillce::binaryorstd::stringas before.
binding::read_bodytakes the codec and the decode options and returns aresult<data_t>.
The parse costs instructions and allocations on receive that v3 spent only when the payload was read.
Receive plus one typed read (from_messagethendata_as) costs the same as in v3, within about 2% in instructions, with equal allocations and slightly fewer retained bytes, because the read reuses the document instead of parsing the text again; each further read is cheaper.
A consumer that never reads the payload, such as a forwarder or router, pays the parse for nothing: on the HTTP full-event path the receive alone costs 15% to 54% more instructions (RapidJSON +15%, Boost.JSON +18%, Glaze +29%, nlohmann +54%) and 8 to 27 more allocations, by codec.
No option skips the parse today:json::decode_options{.retain_document_up_to = 0}keeps the body asjson_textinstead of a document, but the body is still parsed to validate it (SWR-BIND-0007, SWR-BIND-0008), so it saves the retained DOM and not the parse.
Migrate: code that matchedjson_texton a received binary-mode event reads the document (data_as,get<Codec>(),dump()) or sets.retain_document_up_to = 0to keep text; code that relied on a malformed body being accepted now getsparse_errorfromfrom_message. -
json_formatdrops seven public helpers.
required_text,optional_text,read_required,read_optional,read_time,read_context_attributesandread_extensionsare not declared byce::v4::json_format<Codec>.
Nothing in the SDK called them, no suite tested them, and a generation's first tag is the only moment surface can go.ce::v3::json_formatkeeps all seven with their v0.5.0 signatures.
Migrate: read a whole event withdecodeorfrom_value, and a typed payload withdecode_asorfrom_value_as; code that needs one of the helpers itself spellsce::v3::json_format<Codec>and includes<cloudevents/v3/format/json_format.hpp>.
Generations
ce::v4is the inline namespace, soce::eventandce::json_documentnow name the v4 classes. v0.6.0 is its first tag.ce::v3holds the v0.5.0 surface.
The headers that mentionevent,data_torjson_documentare copied underinclude/cloudevents/v3/:core.hpp,format/json_format.hpp,format/typed_payload.hppand the four binding headers.
A v3 declaration does not change, and neither does its documented behaviour; v3 takes defect fixes only.
ce::v3::json_documentdeclares copy operations only, as v0.5.0 did, so moving it copies it and the source keeps its DOM.- What v3 shared stays shared, now with v4 too: the attribute types,
timestamp,message, the literals, the typed extensions,errc,error,result, the codecs and their concepts, base64 and the describe seam are one type throughce::,ce::v4::andce::v3::.
So arejson::decode_options, now declared informat/decode_options.hpp, and the model behind ajson_document. - The optional module exports
ce::v4only. - Events convert between the generations.
ce::v4::from_v3andce::v4::to_v3, declared in the opt-in header<cloudevents/v3_conversion.hpp>, take an event byconst&or&&and return the other generation's event, not aresult, because no conversion can fail.
Ajson_documentconverts in constant time, because both generations' documents point at one model: a conversion copies or moves that pointer and never a DOM.
to_v3of a moved-from v4 document yields a v3 document on the same moved-from model, which dumpsnull.
No other SDK header includes the conversions, and the module does not export them, soimport cloudevents;never brings in the v3 event model. - Decode options reach every binding entry point that decodes JSON (an addition).
from_message<Codec>of HTTP, Kafka and NATS,http::from_batch_message<Codec>andnats::from_payload<Codec>take a defaulted lastjson::decode_optionsand apply it in every mode they read, so existing calls compile unchanged.
json_format::from_valueandfrom_value_astake none: they receive a DOM you already hold.
Faster
- A codec may take over the strings the encoder builds.
A codec that providesadopt_string(std::string&&) -> valuereceives the RFC 3339 text of timestamps and the base64 text of binary data by move instead of by view;ce::json::string_adopting_codec<C>detects it.
It is optional, so a v0.5.0 codec keeps compiling and keeps the copy.nlohmann_codecprovides it, and thece::v3andce::v4encoders both use it.
Measured by the perf job: one allocation fewer inencode_fullon nlohmann (65 to 64), and 100 fewer in a 100-event batch encode on nlohmann (2,824 to 2,724) and on Glaze (715 to 615). Boost.JSON and RapidJSON cannot take over astd::string, so they are unchanged. - A moved
ce::v4::json_documenttouches no reference count.
Move construction goes from one atomic read-modify-write instruction to none, and move assignment from two to one.
Every decode and batch moves events, so they stop counting references. - An error is forwarded by move through one cold helper,
detail::forward_failure, instead of being copied.
It changes no result and no success path: the first version, which moved inline at every site, madeencode_full/rapidjson.shippedrun 2.09 percent more instructions in the perf job, and the cold helper left it at +0.40 percent. - Message bodies are copied in bulk, not one
push_backper byte, into_bytesandto_text. It is a body-only fix that every generation takes (D-CODEC-3). Measured by the perf job (x86_64, g++-14), instructions per operation:
HTTP structured encode 7.4 percent fewer on nlohmann (74,500 to 68,971) and 13.0 to 15.0 percent fewer on the other codecs, HTTP binary decode 4.2 percent fewer, HTTP binary encode 4.3, Kafka binary decode 5.1 and NATS binary decode 5.2. Allocations are unchanged. - A body that owns a
std::stringinstead of astd::vector<std::byte>was measured and declined: it would save at mo...
v0.5.0
An event keeps the JSON document its decoder built, so a typed read or write
parses the payload at most once and never serialises it to text in between.
That changes data_t, so it ships as a third API generation, ce::v3, which is
the inline namespace. ce::v2 keeps what v0.4.0 published, so v0.4.0 code has a
way to keep compiling.
Three generations
ce::v3is the inline namespace, soce::eventnow names the v3 class.ce::v2holds the v0.4.0 surface.
The headers that mentioneventordata_tare copied underinclude/cloudevents/v2/:core.hpp,format/json_format.hpp,format/typed_payload.hppand the four binding headers.
A v2 declaration does not change; v2 takes defect fixes only.- The attribute types are shared by v2 and v3:
id,source,timestamp,message, the literals, the typed extensions and the rest ofattributes.hppare declared once, ince::v2, and are one type throughce::,ce::v2::andce::v3::. - What v0.3.0 already shared stays shared:
errc,error,result, the three codecs, base64 and the describe seam are one type through all three generations. - v3 codecs provide
equal,copy,extract,for_each_mutable_elementandidentity.
ce::v3::json::json_codecadds five members to the v1 concept:C::equal(const value&, const value&) -> bool, JSON value equality with object members in any order;C::copy(const value&) -> value, a deep copy;C::extract(value& object, std::string_view key) -> value, which moves a member out of an object and returns null for a keyfindwould not return;C::for_each_mutable_element(value& array, F visit), which hands each element tovisitas avalue&; andC::identity, a non-emptystatic constexpr std::string_viewnaming the codec.
The three in-tree codecs gain all five, and keep their v1 declarations; their identities areio.cloudevents.cpp.codec.nlohmann,io.cloudevents.cpp.codec.boost_jsonandio.cloudevents.cpp.codec.rapidjson.
A third-party codec must add them to servece::v3, with an identity that is a reverse-DNS name under a domain its author controls and that no other codec uses.
It keeps servingce::v1andce::v2unchanged, since their concept is the v1 one. ce::json_documentholds a parsed JSON document incore.hpp, which still names no codec type.
json_document::make<Codec>(dom)builds one,get<Codec>()returns the DOM to a codec declaring the identity of the one that built it andnullptrto any other, anddump()serialises it.
Documents compare as JSON values, across codecs too.
Copies share one immutable DOM, so a document can be copied, compared and read from several threads at once.
A document is never empty: it has no default constructor, and moving one copies it.data_thas a fifth alternative,json_document.
Code that visitsdata_texhaustively stops compiling againstce::v3until it handles it; that is the migration.
An event holdingjson_textnever equals one holdingjson_document, even for the same JSON value, so equality stays transitive.- A structured decode keeps the payload as a
json_documentbuilt by the decoding codec, where v2 gavejson_text.
decodeanddecode_batchmove thedatamember out of the document they parsed, so the payload is neither serialised nor copied;from_valuecopies it.
A binary-mode body, a non-JSON string payload anddata_base64decode as before. ce::json::decode_options{.retain_document_up_to = 16 * 1024}bounds what one event can pin: input longer than the limit keeps its payload asjson_text, and 0 always does.
The default is 16 KiB, nameddecode_options::default_retention_limit: a retained document measured up to 3.3 times the bytes of its text, so larger payloads stay text unless the caller raises the limit.
decodeanddecode_batchtake it. A batch keeps documents when its text is at most the limit times its number of events, and otherwise every event in it keeps text.- A payload kept as text above the limit is the input's own text of the
datamember, without the whitespace around it, rather than the codec's serialisation of it.
Copying the slice avoids the serialisation, which measured 25 to 49 percent more instructions and 58 to 210 percent more allocated bytes indecode_large.
The decoder falls back to the codec's serialisation when a top-level member name carries an escape, whendataappears twice, or when the input holds something strict JSON does not allow; a batch decides per element.
The text keeps the sender's spelling, so the same JSON formatted differently no longer compares equal asjson_textafter decode. - Encoding a
json_documentbuilt by the encoding codec copies its DOM, with no serialisation and no parse.
A document from another codec converts through the building codec's text. data_as<T, Codec>reads a document built byCodecdirectly, throughget<Codec>(), with no serialisation and no parse.
A document from another codec still converts through text.set_data<T, Codec>stores ajson_documentbuilt byCodec, where v2 storedjson_text.
An event written byset_datatherefore equals the same payload held as a document, never as text.- Typed entry points parse and write once.
ce::decode_as<T, Codec>(text, options)returnsce::decoded<T>{.event, .payload}: the eventdecodereturns, and the payload read from the parseddatamember.
ce::decode_batch_asdoes the same for a batch and fails the whole batch when one payload does not read asT;ce::from_value_asreads a document the caller parsed.
ce::encode_as<T, Codec>(event, payload)writes the payload's DOM underdatawithout changing the event, writingapplication/jsonwhendatacontenttypeis absent and refusing a non-JSON one withtype_mismatch.
They fail asdata_asdoes when the payload is absent (missing_required_attribute) or is not JSON (type_mismatch).
The optional module exports them, andjson::decode_options. - The optional module exports
ce::v3only.
CR-0003 and ADR-0010 record why: the v0.5.0 event model adds an alternative to data_t, and SPEC section 3 rule 4 sends a breaking change to a new generation.
Staying on v0.4.0 code
ce::X is now the v3 entity.
To keep the v0.4.0 declarations, spell ce::v2::X and include the v2/ copy of each header that has one:
#include <cloudevents/v2/binding/http.hpp>
using namespace ce::v2::literals;
auto order = ce::v2::event::builder{.id = "A1"_id, .source = "/orders"_source,
.type = "com.example.placed"_type}.build();
auto request = ce::v2::http::to_message<ce::v2::codec::nlohmann_codec>(
*order, ce::v2::content_mode::binary_mode);Code that names only shared entities, such as an attribute type or a codec, needs no change.
Moving to ce::v3
data_thas a fifth alternative. Add ajson_documentcase wherever you visitdata_t;document.dump()gives its compact text.
Code that readstd::get<ce::json_text>(event.data()).rawafter a structured decode now finds ajson_document: read it withdocument.get<Codec>()for the DOM, ordocument.dump()for text.
Code that compares a decoded event with one built fromjson_textnow compares unequal; build the expected event with ajson_document, or compare the payloads by value.- A codec you wrote needs five more members to serve
ce::v3:equal,copy,extract,for_each_mutable_elementand astatic constexpr std::string_view identity, as listed above.
static_assert(ce::json::json_codec<MyCodec>)fails until all five are present.
Choose an identity under a domain you control; two codecs sharing one would hand each other's DOM to the wrong type.
Without the five members the codec still servesce::v1andce::v2unchanged. decode_optionsbounds what a decoded event pins, 16 KiB by default.
decode,decode_batch,decode_asanddecode_batch_astakece::json::decode_optionsas a second argument; omitting it keeps a document for input up todecode_options::default_retention_limit(16 KiB) and text above it.
Pass{.retain_document_up_to = 0}to keep the v2 behaviour of always decoding to text. The text is now the sender's own spelling of the payload, where v2 gave the codec's serialisation.
Raise the limit when large payloads are read as typed values and memory allows it.set_data<T, Codec>stores a document. Code that readstd::get<ce::json_text>(event.data())afterset_datanow finds ajson_document; read the payload withdata_as, or the text withdump().- Use the typed entry points. Replace
decodefollowed bydata_aswithdecode_as,decode_batchfollowed by a loop ofdata_aswithdecode_batch_as, andset_dataon a copy followed byencodewithencode_as, to save a parse or a serialisation.
Tooling
- Every pull request is measured against main and against budgets.
Theperfworkflow gates on instructions under Callgrind, heap allocations and the bytes a decoded event retains, per operation and codec, and comments the table on the pull request.
Wall time and binary size are reported.
Each merge to main is recorded on thebench-databranch.
docs/PERFORMANCE.md explains the table and how to accept a deliberate cost. - The bench's Boost.JSON codec builds its values with parentheses, so it works with Boost before 1.84.
Faster, in every generation
- A decode reads a document in one pass over its members, instead of one
findper reserved attribute and a second walk for the extensions.
Measured withbench/codec_benchbefore the perf job existed: 7 to 20 percent less CPU time for an event-sized document on all four bench codecs, 7 to 14 percent for a batch of 100. - The bindings stop copying bytes the SDK already holds.
A s...
v0.4.0
A second API generation, ce::v2, in which an invalid CloudEvent cannot be
written down. ce::v1 keeps what v0.3.0 published, so existing code has a way
to keep compiling.
Two generations
ce::v2is the inline namespace, soce::eventnow names the v2 class.ce::v1holds the v0.3.0 surface, underinclude/cloudevents/v1/: the
aggregateevent,validate(),headers, and the format and bindings built
on them. A v1 declaration does not change; v1 takes defect fixes only.- What did not change is shared:
errc,error,result, the codec
concept, the three codecs, base64 and the describe seam are one type through
ce::,ce::v1::andce::v2::. A codec written for v0.3.0 serves both. - The optional module exports
ce::v2only.
CR-0002 and ADR-0009 record why: SPEC section 3 rule 4 sends breaking changes to
a new generation, and v0.4.0's event model is one.
Staying on v0.3.0 code
Spell ce::v1:: and include the v1/ headers. Nothing else changes:
#include <cloudevents/v1/binding/http.hpp>
const ce::v1::event order{.id = "A1", .source = "/orders", .type = "com.example.placed"};
auto request = ce::v1::http::to_message<ce::v1::codec::nlohmann_codec>(
order, ce::v1::content_mode::binary_mode);Moving to ce::v2
v0.3.0 (ce::v1) |
v0.4.0 (ce::v2) |
|---|---|
ce::event{.id = "1", .source = "/s", .type = "t", .subject = "x"} |
ce::event{"1"_id, "/s"_source, "t"_type, {.subject = "x"_subject}} |
| a runtime string assigned to a member | ce::id::make(text) and its siblings, which return a result |
if (auto ok = e.validate(); !ok) |
nothing to call: construction refuses what validate() refused |
e.id, e.subject |
e.id(), e.subject(), returning the validated types |
e.set_extension("name", value) returning result<void> |
e.set_extension("name"_ext, value), which cannot fail |
e.datacontenttype = ...; e.data = ...; |
e.set_data(payload, "application/json"_mediatype) |
ce::headers |
ce::raw_headers; ce::headers is now the type that refuses a repeated field |
binding::read_attributes returning an event |
returns an event::builder; build() reports a missing attribute |
binding::read_body(body, event&) |
read_body(body, media_type) returns the payload |
ce::set_data<T, Codec> returning result<void> |
returns nothing, because it cannot fail |
timestamp::fractional_digits as std::uint8_t |
a fraction_digits, refusing a count above nine |
A literal is checked when the program compiles: ""_id does not build.
docs/GUIDE.md covers the whole v2 surface.
Fixed, in both generations
- A timestamp with more than nine fractional digits divided by zero when
rendered. v2 cannot hold one; v1 renders nine. - The RapidJSON codec handed a null pointer to RapidJSON for an empty string.
- The
result<void>polyfill answerederror()on a success witherrc{0}
wherestd::expectedleaves it undefined; a wrong-branch read now aborts. - base64 accepted any run of padding, so
"QQ======"decoded. - A message carrying one
ce-attribute twice decoded to whichever came last.
It is now refused withinvalid_argument. - A prefixed
datacontenttypeon HTTP or Kafka was accepted as an extension and
then refused as a reserved name. It is now refused asinvalid_argument. - The HTTP percent-encoding policy now refuses ill-formed UTF-8 on the way out,
as it always did on the way in.
Added
docs/GUIDE.md, the user guide. Every C++ block in it is compiled and run in
CI.CONTRIBUTING.mdandSECURITY.md.- A clang-tidy gate over the public headers, including the magic-number checks.
check_references.pychecks the// spec:markers in the headers against
each requirement'ssatisfied_by, in both directions.
Removed
- The Doxygen reference. The headers carry
// spec:markers instead of doc
comments, and the guide is the documentation.
Measured on the release candidate
- Line coverage 95.8% (2416 of 2522), function coverage 92.5%, branch coverage
60.3%, against a floor of 90% lines. - CI: GCC 13 at C++20 and C++23, Clang 16 with libstdc++ 13, Clang 17 with
libc++ 17, AppleClang at C++20 and C++23, MSVC 19, GCC 16 with C++26 static
reflection, plus the forced polyfill, exceptions disabled, no default codec,
every codec on Linux and macOS, ASan with UBSan, and install-and-consume. - 45 CTest entries per preset: 27 for
ce::v2and 18 force::v1, of which
16 are the v0.3.0 suites unchanged.
Known limitations
- libc++ 17 and 18 implement
std::formatwithout defining__cpp_lib_format;
the SDK carves them out by version (D-CI-1). - Clang 16 cannot compile libstdc++ 13 or 14 in C++23 mode, so the C++23 Clang
job uses libc++ (D-CI-1). - The module interface is usable, with constraints on what an importing
translation unit may also include (D-MODULE-1), and it exportsce::v2only. - Given the same non-JSON payload, the Java SDK writes
data_base64where Go
and this SDK write a JSON string. Both are legal (D-INTEROP-1). - HTTP binary mode percent-encodes header values as the binding requires; the Go
and Java SDKs do not, so talking to them needsce::http::literal_values
(D-HTTP-1). - A
ce::v1::timestampcan still be given more than nine fractional digits by
hand. The renderer is guarded; the type is v0.3.0's and does not change.
v0.3.0
v0.3.0
NATS binary mode, and the reason it was missing.
All three bindings now support binary mode: HTTP, Kafka and NATS.
Added
ce::nats::to_message/from_message. Binary mode maps each attribute
to a header named for it with ace-prefix, percent-encoded by HTTP's rule.
It needs a server at NATS 2.2 or later, which is where the protocol gained
message headers.ce::nats::detect_content_mode. The binding inverts HTTP's default: a
CloudEvents content type means structured, and anything else means binary,
including no content type at all.
to_payload and from_payload are unchanged and not deprecated. They remain
the right API for a server before 2.2, which cannot carry a header at all, and
ce::v1 is frozen against removal.
Two rules that differ from every other binding here
- datacontenttype is not special. It maps to
ce-datacontenttypelike any
other attribute, leavingContent-Typeto mean that a message is structured.
The shared binding core gainedcontent_type_is_attributefor this,
detected rather than required, so abinding_traitstype written before
it still satisfies the concept unchanged, including one outside this
repository. - No batch mode, which the binding does not define.
Why this was missing
SWR-NATS-0001 forbade binary mode, quoting the v1.0.2 binding: "the NATS
protocol does not support custom message headers, necessary for binary mode".
NATS 2.2 introduced headers in 2021, and the binding now says "Every compliant
implementation SHOULD support both structured and binary modes".
The requirement was written against a frozen tag, and the tag moved. Nothing in
this repository's gates would catch that: they check a requirement is traceable
and verified, not that the document it was derived from still says the same
thing. Worth knowing if you rely on the traceability here.
Following the specification rather than the only implementation
sdk-go's protocol/nats_jetstream is the only other header-based NATS
mapping and differs from the binding on three rules: content-type unprefixed
instead of ce-datacontenttype, binary detected by ce-specversion presence
rather than content-type absence, and no percent-encoding where the binding
requires it. The third is the same defect as cloudevents/spec#1397 in a
second binding. Reported as cloudevents/sdk-go#1334.
This SDK follows the binding and deliberately ships no compatibility mode for
the divergence: no other SDK has a NATS binary mode yet, so there is nothing to
be compatible with, and adding one would make this the third behaviour rather
than the second.
JetStream
There is no JetStream binding in the CloudEvents specification, so there is
nothing named to implement. JetStream carries the same subjects, headers and
payloads as core NATS; persisting and delivering them is the client's, as
moving any message onto a wire is.
Verified on this commit
- Seven fuzz targets, ten minutes each, no crash, leak or timeout. The NATS
target now drives both decode paths. - AddressSanitizer and UndefinedBehaviorSanitizer clean over the whole suite.
- 150 requirements lint and trace; 435 test suites resolve with no warnings.
- Nine local presets and the full CI matrix, judged by exit code at each stage.
- Go and Java accepted every document this SDK produced.
Known cost
Binary mode follows the NATS binding on main, marked 1.0.3-wip, while the
rest of the SDK implements v1.0.2. If it changes before 1.0.3 is cut, this
follows it (D-NATS-1).
Upgrading from v0.2.0
Nothing is removed and nothing changes behaviour, so code written against
v0.2.0 compiles and behaves as it did.
v0.2.0
v0.2.0
Three JSON codecs to choose between, three protocol bindings, and the cross-SDK
testing that found a defect in the first binding.
Additive throughout: ce::v1 gained entities and lost none, so code written
against v0.1.0 compiles unchanged.
Added
- Codecs.
ce::codec_rapidjsonandce::codec_boost_jsonbeside nlohmann.
CE_CODECStakes any set, or none;CE_DEFAULT_CODECstill works and now
subtracts from the list.README.mdsays which to choose, from whatbench/
measured. - Kafka binding (
ce::kafka): binary and structured mode,ce_prefix,
byte-exact record header keys, unescaped UTF-8 values,to_recordwith an
opt-inpartitionkey_mapper. No batch mode, which the binding spec does not
define. - NATS binding (
ce::nats): structured mode and the JSON event format,
which is all the protocol supports. No subject is derived. ce::binding: what the three bindings share, parameterised by a
binding_traitstype, so a fourth is its traits plus its entry points.ce::http::literal_values: an opt-in value policy for talking to the Go
and Java SDKs. The default still percent-encodes, as the binding spec
requires.headers::find_exact/contains_exact/set_exact, byte-exact beside
the case-insensitive originals.ce::to_bytes/ce::to_textare public.
Fixed
nlohmann_codec::as_intreportsout_of_rangefor an unsigned integer past
INT64_MAXrather than reinterpreting it.2^63used to arrive as a negative
int32that the format layer accepted.- Surrogate pairs combine into one UTF-8 character.
- Boost.JSON values are constructed with parentheses; braces selected the
initializer_listoverload on Boost 1.83 and not on 1.92.
Interoperability, measured 2026-09-21
- The HTTP binding spec requires percent-encoded header values. sdk-go
v2.15.2 and sdk-java implement neither encoding nor decoding; sdk-csharp
does. A conformant sender is therefore silently misread by Go and Java: an
event whosesubjectisa barrives asa%20b. Name
ce::http::literal_valueswhen talking to them.docs/DECISIONS.mdD-HTTP-1
has the tables,cloudevents/spec#1397the cross-SDK picture. - An event that round-trips through the Go SDK loses its UTC offset. Go
rewrites2026-09-20T12:34:56+02:00to...10:34:56Z. The instant survives,
the text does not, andce::timestampcompares by value, so the same moment
can compare unequal. - Of 21 binding-level cases run through sdk-go, 14 agree. The four that differ
are ininterop/audit/README.md, with which specification says who is right.
Verified on this commit
- Coverage 95.9% (1160 of 1209 lines), floor 90%.
- Seven fuzz targets, 649 to 678 seconds each, no crash, leak or timeout.
- AddressSanitizer and UndefinedBehaviorSanitizer clean over the whole suite.
- 150 requirements lint and trace; 434 test suites resolve with no warnings.
- Go and Java accepted every document this SDK produced.
- Built and tested on gcc-13 (C++20 and C++23), clang-16 (C++20, libstdc++ 13),
clang-17 (C++23, libc++), AppleClang (C++20 and C++23), MSVC (C++20), GCC 16
with-freflectionfor C++26 static reflection, and g++-14 for the forced
result<T>polyfill and-fno-exceptionsbuilds.
Known limitations
- libc++ 17 and 18 implement
std::formatwithout defining
__cpp_lib_format; the SDK carves them out by version (D-CI-1). - Clang 16 cannot compile libstdc++ 13 or 14 in C++23 mode, so the C++23 Clang
job uses libc++ (D-CI-1). - The module interface is usable, with constraints on what an importing
translation unit may also include (D-MODULE-1). - Given the same non-JSON payload, the Java SDK writes
data_base64where Go
and this SDK write a JSON string. Both are legal (D-INTEROP-1). - Boost.JSON requires exceptions; the build refuses it under
-fno-exceptions
at configure time.
cloudevents-cpp v0.1.0
CloudEvents v1.0.2 for C++20: the core event model, the JSON event format, the HTTP protocol binding, and a typed layer over both.
Header-only, no exceptions, no network I/O. The SDK names no HTTP type and picks no JSON library for you — ce::json::json_codec is a concept, and an nlohmann codec ships beside it.
This tag freezes the ce::v1 API. A breaking change after it introduces a new version namespace (SWR-BUILD-0006).
#include <cloudevents/binding/http.hpp>
#include <cloudevents/codec/nlohmann.hpp>
using codec = ce::codec::nlohmann_codec;
ce::event order{
.id = "A234-1234-1234",
.source = ce::uri_ref{"https://example.test/orders"},
.type = "com.example.order.placed",
};
order.datacontenttype = "application/json";
order.data = ce::json_text{.raw = R"({"total":42})"};
auto request = ce::http::to_message<codec>(order, ce::content_mode::binary_mode);Measured on this commit, not asserted
| Line coverage | 95.9% (1021 of 1065) |
| Fuzzing | 5 targets, 10 minutes each, 338.3M executions, clean |
| Sanitizers | full suite under ASan and UBSan, clean |
| Interoperability | the Go and Java SDKs accept every document this SDK produces, and their goldens decode here |
| Conformance | all 19 examples from the CloudEvents core, JSON format and HTTP binding documents, stored verbatim |
| Requirements | 122, every one with test evidence that resolves |
Per-target fuzz executions: timestamp 186.4M, base64 123.9M, json_decode 6.3M, http_message 14.2M, typed 7.4M.
Toolchains exercised
GCC 13 and 16, Clang 16 with libstdc++ 13, Clang 17 with libc++ 17, AppleClang, MSVC 19. C++20 and C++23, with and without std::expected, with and without exceptions, with and without the default codec, plus C++26 static reflection.
What is in it
- Three content modes — binary, structured and batched, both directions, over a transport-neutral
ce::message. - Typed extensions — the five documented CloudEvents extensions as structs.
event.get<ce::ext::tracing>()restores the declared field type even when the wire form threw it away, which HTTP binary mode always does. - Typed payloads — describe your own struct once:
set_data<order>(event, value),data_as<order>(event),event_of<order, codec>. - Your JSON library — a codec is a struct of statics over your DOM;
examples/custom_codec.cppis a complete one. - Four targets with a strictly downward dependency graph.
ce::coredepends on CTRE alone, checked by a test built against the installed package.
Installing
cmake -S . -B build && cmake --build build --target installfind_package(cloudevents REQUIRED) # core, format, binding
find_package(cloudevents REQUIRED COMPONENTS codec_nlohmann) # and the codecKnown limitations
Each recorded in docs/DECISIONS.md with the measurement behind it.
D-CI-1— libc++ 17 and 18 implementstd::formatwithout defining__cpp_lib_format, so the SDK carves them out by version. Clang 16 cannot compile libstdc++ 13 or 14 in C++23 mode at all.D-MODULE-1—cloudevents.cppmis usable, but an importing translation unit may not also include a standard header its global module fragment absorbed. It stays out of the default build.D-INTEROP-1— given the same non-JSON payload, the Java SDK writesdata_base64where Go and this SDK write a JSON string. Both are legal. Compare bytes, not representation.D-ADOPT-1— nlohmann is looked up softly, sofind_package(cloudevents)works without it. Ask for thecodec_nlohmanncomponent when you need the codec.
Licence
Apache-2.0, matching the other CloudEvents SDKs.