Task: Migrate remaining service-to-service hard-coded rfl::json call sites to wire_codec
Table of Contents
This page documents a task in the Make NATS wire format configurable: JSON/MessagePack, decided once at startup story. It captures the goal, current status, acceptance, and any notes or results.
Goal
Every remaining NATS request/reply and publish/subscribe call site
that still hard-codes rfl::json::write=/=read for its wire payload
is migrated onto ores::nats::default_wire_codec(), so a
process-wide switch to wire_format::msgpack is safe repo-wide, not
just for the six task lists already closed in this story.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Make NATS wire format configurable: JSON/MessagePack, decided once at startup |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-30 |
Acceptance
- A repo-wide sweep for
rfl::json::write=/=readco-located with a NATS transport call (request_sync,.request(,authenticated_request(,.publish(,js_publish() shows only intentionally-excepted embedded/persisted-JSON fields remaining (documented indoc/knowledge/architecture/nats_wire_format.org), not genuine wire-payload hardcodes. - Full-repo build and
ctestpass (74/74 test binaries).
Plan
Started from the task's own named list (6 files + the
ClientManagerTradeInstrument.cpp decode side), then broadened via a
repeated sweep (grep for a NATS transport call co-located with
rfl::json::write=/=read) since the discovery grep undercounted —
many hits are duplicated per-service helpers (e.g. a
publish_entity_event copy-pasted into 6 different
application.cpp files) or matched publish/subscribe pairs across
different components.
For every hit, classified it as either:
- Genuine wire payload — the bytes handed to
publish=/=request=/=authenticated_request=/=js_publish— migrate tocodec.encode()=/=codec.decode(). - Embedded/persisted JSON field — a
std::stringfield whose value happens to be JSON (workflowcommand_json=/=result_json, scheduleraction_payload, nestedrequest_jsoninner payloads, or the JWKS bootstrap exchange) — left untouched; these are a category error to migrate, not an oversight. Seedoc/knowledge/architecture/nats_wire_format.orgfor the full rationale and the list of recognised exceptions.
Fixed pairs together in the same change wherever a publisher and its
matching subscriber both needed migrating (else the pair would break
under msgpack): entity_change_event publishers across 6 service
application.cpp files; work_assignment_event between
ores.compute.core::workunit_handler=/=report_submit_handler
(producers) and ores.compute.wrapper::application.cpp (consumer);
fx_spot_tick=/=ir_curve_tick between ores.synthetic.service's
feed publishers and ores.marketdata.service's ingest loops;
step_completed_event and start_workflow_message between
ores.service::messaging::workflow_helpers /
ores.workflow.core::workflow_handler.cpp (producers) and
ores.workflow.core::workflow_engine.cpp (consumer) — the latter
pair was a real, previously-unnoticed mismatch (producer already used
wire_codec, consumer still hard-coded rfl::json::read).
Also closed the ClientManagerTradeInstrument.cpp gap this task was
named for: parse_trade_instrument.cpp and
parse_swap_instruments.cpp (ores.qt.headless) now decode via
ores::nats::default_wire_codec() instead of a hard-coded
rfl::json::read per wrapper type in their two-phase parse. This
needed a new ores.qt.headless -> ores.nats.lib link dependency
(CMakeLists.txt), regenerated via
projects/ores.codegen/scripts/regenerate_cmake_component_files.py
where headers moved between components (none did here, this dependency
was link-only).
Documented the payload-vs-envelope model and the wire-vs-embedded-JSON
distinction in a new architecture doc
(doc/knowledge/architecture/nats_wire_format.org), since this
distinction is easy to get wrong when auditing future call sites.
Notes
Re-running the sweep after every fix kept surfacing new hits because
the initial discovery grep (rfl::json + request_sync) missed
publish=/=js_publish-only sites and single-direction-typed helpers.
The final sweep command (see the architecture doc's "Practical audit
pattern" section) is broader and should be the standard tool for any
future audit before a wire-format-sensitive change.
Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario, run
through the QA Validation Runner panel) that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
PRs
| PR | Title |
|---|---|
| #1785 | [ores.nats] Migrate remaining rfl::json call sites to wire_codec |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | clang-format drift on wrapped calls (over column limit, misaligned continuation) | workflow_engine.cpp, dataset_handler.hpp, publication_handler.hpp, ore_import_handler.cpp, workflow_handler.cpp | Accepted | Ran clang-format –style=file over the flagged files. |
| 2 | Unused #include <span> left behind now that codec.encode() returns std::vector<std::byte> directly |
dataset_handler.hpp, publication_handler.hpp | Accepted | Removed; also swept remaining changed files for the same pattern and found 2 more (trade_handler.hpp, compute/wrapper/app/application.cpp), removed those too. |
| 3 | nats_publish_action_handler.cpp's nats_publish_payload=/=nats_trigger_body locally redeclare the same fields as trigger_report_instance_message instead of reusing the shared type |
nats_publish_action_handler.cpp | Declined | Pre-existing, not introduced by this PR – reviewer explicitly flagged as not blocking; a follow-up cleanup task if this duplication pattern recurs, not this PR's scope. |
Result
Migrated ~40 genuine wire-payload call sites across ores.iam,
ores.marketdata (client, core, service), ores.refdata,
ores.reporting, ores.trading, ores.workflow, ores.compute
(core, service, wrapper), ores.dq, ores.eventing, ores.scheduler,
ores.synthetic, ores.ore, ores.analytics, ores.workspace and
ores.qt.headless onto ores::nats::default_wire_codec(), including
the ClientManagerTradeInstrument.cpp two-phase decode this task was
originally named for. Fixed one real producer/consumer wire-format
mismatch found along the way (workflow_engine.cpp's
on_step_completed=/=on_start_workflow still hard-coded JSON while
their publishers already used wire_codec). Left ~15 embedded/
persisted-JSON-field call sites untouched by design (workflow
command_json=/=result_json, scheduler action_payload, nested
request_json fields, JWKS bootstrap) and documented the rationale in
a new architecture doc,
doc/knowledge/architecture/nats_wire_format.org. Full-repo build and
ctest both pass (74/74 test binaries, 100%). A final repo-wide sweep
confirms no genuine wire-payload rfl::json hardcodes remain.