Severity: Low-Medium — reachable via the shipped sjq/syq CLIs (unlike #1678), but only
for the narrow #1194 shape (a key the format's grammar never allowed at all, e.g. a bare
non-string JSON key {123: 1}), not the far more common #1247/#1642 undecodable-key shape,
which this branch already fixed everywhere.
Summary
Found while reviewing/fixing #1642, which closed the "same document, different answer"
inconsistency for a key whose bytes won't decode (#1247's fault). That fix left a
structurally-analogous, pre-existing gap open on the adjacent #1194 axis: a key the format's
grammar never allowed at all still raises on some routes and silently vanishes on others.
keys() (src/jq/document.rs), effective_keys, to_owned_at_depth,
to_owned_cursor_at_depth, to_owned_with_comments_at_depth, and the ToEntries builtin
(src/jq/eval_generic.rs) all do:
let Some(key) = key_display_string(&field.key) else {
return Err(f.malformed_member_error());
};
key_display_string returns None exactly for a #1194 key (see its doc comment), so all of
these correctly raise.
But lazy_keys_array_to_owned and cursor_to_owned_at_depth (src/jq/lazy.rs), and
stream_lazy_keys_json/stream_lazy_keys_yaml (src/jq/stream.rs), instead do:
if let Some(key) = key_display_string(&field.key) {
// push / write
}
with no else — a None (the #1194 case) just silently drops the field, no error. This
predates #1642 (the old code had the identical asymmetry via field.key_str()/if let StandardJson::String(k) = key), so #1642 didn't introduce it — but its "one document, many
answers" framing makes this easy to mistake as already closed, since it's exactly that bug
class, just on the #1194 axis instead of #1247's.
Repro
$ echo '{123:1,"a":1}' | sjq -c 'keys' # raises (correct)
$ echo '{123:1,"a":1}' | sjq -c 'keys_unsorted' # ["a"] <- silently one entry short, exit 0
$ echo '{123:1,"a":1}' | sjq -c 'length' # 2
keys and length/keys_unsorted disagree with each other on the same document, depending
on which builtin is asked — the same failure mode #1642's own issue description named.
Suggested direction
Not attempted; same two-way choice #1642 itself faced (raise everywhere vs. skip/preserve
everywhere), and picking "raise everywhere" here is more invasive than #1642's fix was:
stream_lazy_keys_json/stream_lazy_keys_yaml write through a core::fmt::Write-style
signature with no error channel today, the same constraint that made #1247's original
design doc (docs/plan/decode-failure-routing.md) split out and defer the value-side
streaming fix as its own stage rather than attempt it inline. lazy_keys_array_to_owned's
signature already returns Result, so that one site at least is a smaller lift.
Related
Severity: Low-Medium — reachable via the shipped
sjq/syqCLIs (unlike #1678), but onlyfor the narrow #1194 shape (a key the format's grammar never allowed at all, e.g. a bare
non-string JSON key
{123: 1}), not the far more common #1247/#1642 undecodable-key shape,which this branch already fixed everywhere.
Summary
Found while reviewing/fixing #1642, which closed the "same document, different answer"
inconsistency for a key whose bytes won't decode (#1247's fault). That fix left a
structurally-analogous, pre-existing gap open on the adjacent #1194 axis: a key the format's
grammar never allowed at all still raises on some routes and silently vanishes on others.
keys()(src/jq/document.rs),effective_keys,to_owned_at_depth,to_owned_cursor_at_depth,to_owned_with_comments_at_depth, and theToEntriesbuiltin(
src/jq/eval_generic.rs) all do:key_display_stringreturnsNoneexactly for a #1194 key (see its doc comment), so all ofthese correctly raise.
But
lazy_keys_array_to_ownedandcursor_to_owned_at_depth(src/jq/lazy.rs), andstream_lazy_keys_json/stream_lazy_keys_yaml(src/jq/stream.rs), instead do:with no
else— aNone(the #1194 case) just silently drops the field, no error. Thispredates #1642 (the old code had the identical asymmetry via
field.key_str()/if let StandardJson::String(k) = key), so #1642 didn't introduce it — but its "one document, manyanswers" framing makes this easy to mistake as already closed, since it's exactly that bug
class, just on the #1194 axis instead of #1247's.
Repro
keysandlength/keys_unsorteddisagree with each other on the same document, dependingon which builtin is asked — the same failure mode #1642's own issue description named.
Suggested direction
Not attempted; same two-way choice #1642 itself faced (raise everywhere vs. skip/preserve
everywhere), and picking "raise everywhere" here is more invasive than #1642's fix was:
stream_lazy_keys_json/stream_lazy_keys_yamlwrite through acore::fmt::Write-stylesignature with no error channel today, the same constraint that made #1247's original
design doc (
docs/plan/decode-failure-routing.md) split out and defer the value-sidestreaming fix as its own stage rather than attempt it inline.
lazy_keys_array_to_owned'ssignature already returns
Result, so that one site at least is a smaller lift.Related
left this jq: a malformed object field (e.g. {invalid}) silently disappears instead of erroring #1194 axis open.
EvalError-raising convention the fixed call sites already use.