diff --git a/include/sonic/internal/arch/simd_skip.h b/include/sonic/internal/arch/simd_skip.h index 1f4ec25..d0a0bfa 100644 --- a/include/sonic/internal/arch/simd_skip.h +++ b/include/sonic/internal/arch/simd_skip.h @@ -959,15 +959,21 @@ class SkipScanner2 { } if (found) { - // if not null - if (peek() != 'n') { - return getJsonPath( - path, index + 1, jsonGenerator, jsonGeneratorFactory); - } else { + // A null value only has to be skipped when the path continues past it, + // because nothing can be selected out of null. When the key is the last + // step, null is the result the caller asked for, so recurse and let the + // path_is_nil branch emit it -- exactly what the array element path + // already does for e.g. "$[0]" over "[null]". + // Under FLATTEN/QUOTE this call is collecting wildcard results, where + // nulls are deliberately dropped (see JsonPath.WildcardFiltersNull), + // so the skip must stay unconditional there. + if (peek() == 'n' && (style != RAW || index + 1 < path.size())) { // skip null RETURN_FALSE_IF_PARSE_ERROR(skipOne()); return false; } + return getJsonPath( + path, index + 1, jsonGenerator, jsonGeneratorFactory); } return false; } diff --git a/tests/jsonpath_test.cpp b/tests/jsonpath_test.cpp index acf9154..5894d71 100644 --- a/tests/jsonpath_test.cpp +++ b/tests/jsonpath_test.cpp @@ -415,7 +415,8 @@ TEST(JsonPath, BadCases) { TEST(JsonPath, KeyNullElementPair) { auto json = R"( { "last_name": "Kim", "first_name":"Sam", "gender": false, "income": null, "age": 38} )"; - TestOk(json, "$.income", ""); + TestOk(json, "$.income", + "null"); // null object member returned, not skipped (aligns with DOM) } TEST(JsonPath, KeyIntoStringValue) { @@ -767,6 +768,53 @@ TEST(JsonPathDom, NotFoundAndUnsupportedPath) { EXPECT_EQ(std::get<0>(unsupported), ""); } +TEST(JsonPathOnDemand, NullObjectMemberIsReturnedNotSkipped) { + // A null object member is a value, not a missing member. The on-demand + // backend skipped it whenever a key matched, so $.a over {"a":null} reported + // no match while the DOM backend returned "null" for the same input. + // The null only has to be skipped when the path continues past it, since + // nothing can be selected out of null. + struct Case { + const char* json; + const char* path; + }; + for (const auto& c : + {Case{R"({"a":null})", "$.a"}, Case{R"({"a":null,"b":1})", "$.a"}, + Case{R"({"b":1,"a":null})", "$.a"}, + Case{R"({"a":{"b":null}})", "$.a.b"}, + Case{R"({"a":[{"b":null}]})", "$.a[0].b"}}) { + auto od = GetByJsonPathOnDemand(c.json, c.path); + EXPECT_EQ(std::get<1>(od), kErrorNone) << c.json << " " << c.path; + EXPECT_EQ(std::get<0>(od), "null") << c.json << " " << c.path; + // Both backends must agree. + auto dom = GetByJsonPath(c.json, c.path); + EXPECT_EQ(std::get<1>(dom), std::get<1>(od)) << c.json << " " << c.path; + EXPECT_EQ(std::get<0>(dom), std::get<0>(od)) << c.json << " " << c.path; + } + + // Arrays already behaved correctly; keep them covered so the two stay + // aligned. + for (const auto& c : {Case{"[null]", "$[0]"}, Case{"[1,null]", "$[1]"}}) { + auto od = GetByJsonPathOnDemand(c.json, c.path); + EXPECT_EQ(std::get<1>(od), kErrorNone) << c.json << " " << c.path; + EXPECT_EQ(std::get<0>(od), "null") << c.json << " " << c.path; + } + + // The path continuing past a null must still not match. + for (const auto& c : + {Case{R"({"a":null})", "$.a.b"}, Case{R"({"a":null})", "$.a[0]"}, + Case{R"({"a":{"b":null}})", "$.a.b.c"}}) { + auto od = GetByJsonPathOnDemand(c.json, c.path); + EXPECT_NE(std::get<1>(od), kErrorNone) << c.json << " " << c.path; + } + + // Wildcard collection deliberately drops nulls; that must not change. + auto wc = GetByJsonPathOnDemand( + R"({"root": [{"a":null},{"a":"foo"},{"a":"bar"}]})", "$.root[*].a"); + EXPECT_EQ(std::get<1>(wc), kErrorNone); + EXPECT_EQ(std::get<0>(wc), R"(["foo","bar"])"); +} + TEST(JsonPathDom, NegativeIndexSupportedButOnDemandUnsupported) { auto dom_got = GetByJsonPath("[0,1,2]", "$[-1]"); EXPECT_EQ(std::get<1>(dom_got), kErrorNone);