Repository navigation
fix(jsonpath): return a null object member instead of skipping it on-demand - #153
linhongyu510 wants to merge 2 commits into
Conversation
…demand
GetByJsonPathOnDemand reported no match for a null object member, while
GetByJsonPath returned "null" for the same document and path:
{"a":null} $.a DOM "null" on-demand no match
{"a":{"b":null}} $.a.b DOM "null" on-demand no match
Arrays were unaffected -- $[0] over [null] already returned "null" -- so the
two backends disagreed depending only on whether the null sat under a key or
an index.
In the object branch, once advanceKey() matched, a null value was skipped
unconditionally. Skipping is only correct while the path still has steps left,
because nothing can be selected out of null; when the key is the final step the
null *is* the requested value. The array element path never had this problem
because it recurses straight into getJsonPath(), where the path_is_nil branch
emits the value.
The skip is kept unconditional for FLATTEN/QUOTE, which is how wildcard
collection runs: JsonPath.WildcardFiltersNull asserts $.root[*].a drops nulls
and yields ["foo","bar"], and that behaviour is unchanged. Only RAW, the
single-result style, now distinguishes "path ends here" from "path continues".
Found with a differential fuzzer that generates random documents plus every
path into them and compares the two backends. Before: 3928 mismatches out of
135242 (json, path) pairs, all of them a null object member. After: 0.
Also replayed all 94 path literals from tests/jsonpath_test.cpp against 11
documents (1034 rows): byte-identical to upstream, since none of those
documents pairs a key directly with null.
|
Confirmed these two PRs are safe to merge in either order. I test-merged #152 and #153 onto With both applied, verified the combined behaviour:
|
The on-demand backend now returns "null" for a null object member instead of skipping it (aligned with the DOM backend, see NullObjectMemberIsReturnedNotSkipped). The pre-existing KeyNullElementPair still asserted the old empty-string expectation, so it failed locally.
|
F pushed a small test-only follow-up ( |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #153 +/- ##
=======================================
Coverage 69.67% 69.67%
=======================================
Files 27 27
Lines 3706 3706
Branches 1189 1189
=======================================
Hits 2582 2582
Misses 211 211
Partials 913 913 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The bug
GetByJsonPathOnDemandreports no match for a null object member, whileGetByJsonPathreturns"null"for the same document and path.Arrays are not affected —
$[0]over[null]already returns"null"— so the two backends disagree based only on whether the null sits under a key or an index. A caller switching to the on-demand API for performance silently loses every explicit-null field.Root cause
In the object branch, once
advanceKey()matches, a null value is skipped unconditionally:Skipping is right only while the path still has steps left, since nothing can be selected out of null. When the key is the final step, the null is the requested value. The array element path never had this problem because it recurses straight into
getJsonPath(), where thepath_is_nilbranch emits the value throughgetOne().The fix, and a trap in it
One condition, plus a
styleguard that matters:My first attempt used only
index + 1 < path.size()and brokeJsonPath.WildcardFiltersNull, which asserts that$.root[*].aover{"root":[{"a":null},{"a":"foo"},{"a":"bar"}]}yields["foo","bar"]— nulls dropped, not[null,"foo","bar"]. The same null-skip is doing double duty: guarding traversal and filtering wildcard results. Wildcard collection runs underFLATTEN/QUOTE, so the skip stays unconditional there and onlyRAW, the single-result style, distinguishes "path ends here" from "path continues". That test now passes unchanged, and I added it to the new test as an explicit guard.How it was found
A differential fuzzer: generate a random JSON document, record every path that resolves into it, then compare both backends on each. Any disagreement is a bug in one of them, with no oracle needed.
Every one of the 3928 had DOM returning
null, which is what reduced the whole class to the one-line cause above.Verification
Since there is no local gtest here and
FetchContentcannot reach the network from this machine, I verified by differential replay rather than running the suite — worth knowing when reading the evidence.tests/jsonpath_test.cpp× 11 documents = 1034 rows, byte-identical to upstream. None of those documents pairs a key directly with null, which is precisely why the suite did not catch this.$.root[*].awhere every value is null) returns err 20 rather than[]— I checked, and that is pre-existing upstream behaviour, not something this change introduces. I have left it alone.The new test asserts all four directions: null returned for a terminal key, both backends agreeing, path-continues-past-null still not matching, and wildcard nulls still filtered.
clang-formatclean (14.0.6).Relationship to #152
Independent. #152 makes the on-demand negative-index limitation reportable (
kErrorNoneNoMatch→kUnsupportedJsonPath); this one fixes a case where on-demand returns the wrong answer outright. They touch different functions and can be merged in either order — I kept them as separate branches offmasterrather than stacking them.