Skip to content

fix(json): keep nested bytes/ints tagged in qvariantToNlohmann containers#23

Merged
dlipicar merged 1 commit into
masterfrom
fix/qvariant-nested-bytes
Jul 17, 2026
Merged

fix(json): keep nested bytes/ints tagged in qvariantToNlohmann containers#23
dlipicar merged 1 commit into
masterfrom
fix/qvariant-nested-bytes

Conversation

@dlipicar

Copy link
Copy Markdown
Collaborator

Problem

A bstr method argument never reaches a cdylib (Rust) module — the callee decodes an empty Vec<u8> and, e.g., echoBytes(v) returns null.

Root cause is in qvariantToNlohmann (cpp/logos_json_convert.cpp). It ran the canConvert<QJsonObject>() / canConvert<QJsonArray>() fallbacks before the type-preserving QVariantList/QVariantMap recursion. A QVariantList/QVariantMap also reports canConvert<QJson…>() == true, so a container was routed through QJson — which has no byte type and degrades numerics to double. A nested QByteArray was flattened to a plain string, losing the canonical {"_bytes":"<base64url>"} tag. (Nested whole integers happened to survive — they serialize without a decimal point — which is why the existing int-preservation tests passed while bytes silently broke, and why the container recursion added earlier was effectively shadowed/dead for these types.)

How it bites

LogosProviderObject::callMethodStdBridge converts every call argument via qvariantToNlohmann. A bstr argument arrives (over QtRO) as a QByteArray nested inside the QVariantList of call args:

transport delivers:  QVariantList[ QByteArray("hello") ]
qvariantToNlohmann:   ["hello"]            ← tag lost (was [{"_bytes":"aGVsbG8"}])
cdylib bytes::decode: not a {"_bytes":…} object → None → empty Vec → echoBytes → null

The QtRO C++ path is unaffected (native QByteArray marshaling; Qt auto-converts QStringQByteArray), and the plain-lp path was already correct and tested. Only the container-through-QVariant leg dropped the tag.

Fix

Move the container recursion (QStringList / QVariantList / QVariantMap) ahead of the QJson fallbacks, so nested elements recurse element-by-element — QByteArray{"_bytes":…} (tagged), integers stay integers, etc. Only genuine QJson-typed QVariants reach the fallbacks now. One-block reorder; no behavior change for scalars or already-QJson variants.

Tests

  • 3 new JsonConvertBytes unit tests: nested bytes in a list, nested bytes in a map, and the exact callMethodStdBridge shape (QVariantList[QByteArray][{"_bytes":…}] → re-decodes to QByteArray). All fail before / pass after.
  • Full protocol suite: 163/163 pass (existing int-preservation and [tstr] tests unaffected).
  • End-to-end: with this protocol built into logoscore, the Rust test_fullapi_rust.echoBytes 'json:{"_bytes":"aGVsbG8"}' now returns the bytes ({"_bytes":"aGVsbG8"}) instead of null; instrumentation confirmed callMethodStdBridge now yields [{"_bytes":"aGVsbG8"}] instead of ["hello"].

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 17, 2026 22:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes lossless JSON conversion for QVariant containers so nested QByteArray (and integer) values remain type-preserved when converting through qvariantToNlohmann, which is critical for the Qt↔Rust cdylib bridge and other C-ABI consumers.

Changes:

  • Reordered qvariantToNlohmann to run QStringList/QVariantList/QVariantMap recursion before the QJsonObject/QJsonArray conversion fallbacks, preventing nested bytes/ints from being coerced by Qt JSON.
  • Added unit tests covering nested QByteArray inside lists/maps and a list-shaped argument case to ensure round-trip preservation via nlohmannArgsToQVariantList.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
cpp/logos_json_convert.cpp Moves Qt JSON fallbacks after container recursion to preserve nested byte/int types during conversion.
tests/protocol/test_json_convert_bytes.cpp Adds regression tests verifying nested bytes remain tagged and can round-trip back to QByteArray.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +100 to +108
// Nested bytes inside a container must keep the tagged form. Regression for the
// cdylib method-argument path: LogosProviderObject::callMethodStdBridge feeds
// every call argument through qvariantToNlohmann, and a bstr argument arrives
// (over QtRO) as a QByteArray nested inside the QVariantList of call args.
// Before the fix, the canConvert<QJsonArray>/<QJsonObject> fallbacks caught the
// container FIRST and routed it through QJson, which has no byte type — the
// nested QByteArray was flattened to a plain string, so a Rust cdylib's
// {"_bytes":...} decoder saw "hello" and produced an empty Vec (echoBytes → null).
TEST(JsonConvertBytes, NestedByteArrayInListStaysTagged)
Comment on lines +142 to +145
// Exactly the callMethodStdBridge shape: the outer call arg is a
// QVariantList holding one bstr param as a QByteArray. qvariantToNlohmann
// must yield [{"_bytes":...}] so the callee (or the next hop's
// nlohmannArgsToQVariantList) recovers a QByteArray, not the string "hello".
…ners

qvariantToNlohmann ran its canConvert<QJsonObject>/<QJsonArray> fallbacks BEFORE
the type-preserving QVariantList/QVariantMap recursion. A QVariantList/QVariantMap
also reports canConvert<QJson*>()==true, so a container was routed through QJson —
which has no byte type and degrades numerics to double. A nested QByteArray was
therefore flattened to a plain string, losing the canonical {"_bytes":...} tag.

Concretely this broke bstr method ARGUMENTS to cdylib (Rust) modules:
LogosProviderObject::callMethodStdBridge feeds each call arg through
qvariantToNlohmann, and a bstr arg arrives (over QtRO) as a QByteArray nested in
the QVariantList of call args. It was flattened to "hello", so the cdylib's
{"_bytes":...} decoder produced an empty Vec (e.g. echoBytes returned null). The
QtRO C++ path was unaffected (native QByteArray marshaling) and the plain-lp path
was already correct; only the container-through-QVariant leg dropped the tag.

Fix: move the container recursion (QStringList/QVariantList/QVariantMap) ahead of
the QJson fallbacks so nested elements recurse element-by-element (bytes stay
tagged, integers stay integers); only genuine QJson-typed variants reach the
fallbacks. Adds nested-bytes-in-list/map + bridge-shape regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dlipicar
dlipicar force-pushed the fix/qvariant-nested-bytes branch from 417a7b3 to eced852 Compare July 17, 2026 22:34
@dlipicar

Copy link
Copy Markdown
Collaborator Author

Good catch on both — the code was fine but the test comments overstated the shape. Tightened them:

  • callMethodStdBridge converts each argument individually, so a top-level QByteArray argument already hits the tagged case. The break is specifically an argument that is itself a QVariantList/QVariantMap containing a QByteArray — in the cdylib path, callModuleMethod's 3rd argument (the nested call-args list), which callMethodStdBridge hands to qvariantToNlohmann as a single QVariantList. Reworded the block comment to say exactly that.
  • Dropped the "exactly the callMethodStdBridge shape" phrasing on the list-valued-arg test; it now describes it as a list-valued argument containing a bstr (the shape of that 3rd arg), not the bridge's per-element iteration.

Comment-only amend; tests and behavior unchanged.

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.

2 participants