From 6118c164ad85a1183e91bf4161ffcc9bffd4831e Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Sat, 5 Sep 2026 04:51:11 +0000 Subject: [PATCH 1/5] Fix get_json_object dropping object fields that follow an empty key parse_name used a zero-length name as the signal that no name was present, so the legal JSON key "" was treated as an absent name and the following colon was left unconsumed. The element-type switch then saw ':' and failed the whole row, making every field after an empty key unreachable. Use parse_string's result code to decide whether a name was present, which distinguishes a present-but-empty name from an absent one. --- cpp/src/json/json_path.cu | 21 ++++++++++----------- cpp/tests/json/json_tests.cpp | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/cpp/src/json/json_path.cu b/cpp/src/json/json_path.cu index ff74c2167dde..544cd24ee903 100644 --- a/cpp/src/json/json_path.cu +++ b/cpp/src/json/json_path.cu @@ -355,7 +355,9 @@ class json_state : private parser { * The user can specify whether or not the name string must be present via * the `can_be_empty` flag. * - * When a name is present, it must be followed by a colon `:` + * When a name is present, it must be followed by a colon `:`. Note that a name that is + * present but zero-length (the `""` key of `{"":1}`) is distinct from an absent name, so + * presence is determined by the `parse_string` result rather than by the name's length. * * @param[out] name The resulting name. * @param can_be_empty Parameter indicating whether it is valid for the name @@ -366,17 +368,14 @@ class json_state : private parser { { char const quote = options.get_allow_single_quotes() ? 0 : '\"'; - if (parse_string(name, can_be_empty, quote) == parse_result::ERROR) { - return parse_result::ERROR; - } + auto const result = parse_string(name, can_be_empty, quote); + if (result != parse_result::SUCCESS) { return result; } - // if we got a real string, the next char must be a : - if (name.size_bytes() > 0) { - if (!parse_whitespace()) { return parse_result::ERROR; } - if (*pos == ':') { - pos++; - return parse_result::SUCCESS; - } + // a name is present, so the next char must be a : + if (!parse_whitespace()) { return parse_result::ERROR; } + if (*pos == ':') { + pos++; + return parse_result::SUCCESS; } return parse_result::EMPTY; } diff --git a/cpp/tests/json/json_tests.cpp b/cpp/tests/json/json_tests.cpp index 1d201844a963..e2759d1f1e19 100644 --- a/cpp/tests/json/json_tests.cpp +++ b/cpp/tests/json/json_tests.cpp @@ -1044,6 +1044,23 @@ TEST_F(JsonPathTests, QueriesContainingQuotes) do_test(R"($.'A)", R"({"B'": 3})"); } +TEST_F(JsonPathTests, ObjectWithEmptyKey) +{ + // A zero-length key is a legal JSON name. Scanning past it must not abort the row, so fields + // that follow an empty key have to remain reachable. + auto const input = cudf::test::strings_column_wrapper{R"({"":0,"a":1})", + R"({"a":1,"":0})", + R"({"" : 0, "a" : 1})", + R"({"":{"a":9},"a":1})", + R"({"":[1,2],"a":1})", + R"({"":"s","a":1})"}; + + auto const result = + cudf::get_json_object(cudf::strings_column_view(input), std::string_view{"$.a"}); + auto const expected = cudf::test::strings_column_wrapper{"1", "1", "1", "1", "1", "1"}; + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*result, expected); +} + // Test that get_json_object creates valid string columns for empty/whitespace JSONPath queries TEST_F(JsonPathTests, EmptyPathCreatesValidColumn) { From de22fd8d85867c86db965592b50d28de46612f39 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Tue, 8 Sep 2026 18:15:47 +0000 Subject: [PATCH 2/5] Shorten parse_name docstring note --- cpp/src/json/json_path.cu | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cpp/src/json/json_path.cu b/cpp/src/json/json_path.cu index 544cd24ee903..941592af75ee 100644 --- a/cpp/src/json/json_path.cu +++ b/cpp/src/json/json_path.cu @@ -355,9 +355,8 @@ class json_state : private parser { * The user can specify whether or not the name string must be present via * the `can_be_empty` flag. * - * When a name is present, it must be followed by a colon `:`. Note that a name that is - * present but zero-length (the `""` key of `{"":1}`) is distinct from an absent name, so - * presence is determined by the `parse_string` result rather than by the name's length. + * When a name is present, it must be followed by a colon `:`. A present but zero-length + * name (the `""` key of `{"":1}`) is not an absent name. * * @param[out] name The resulting name. * @param can_be_empty Parameter indicating whether it is valid for the name From 44a1c9c6f300f7765e2679ce2e318d54cf3fb388 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Tue, 8 Sep 2026 18:18:00 +0000 Subject: [PATCH 3/5] Scope parse_string result to the if statement --- cpp/src/json/json_path.cu | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cpp/src/json/json_path.cu b/cpp/src/json/json_path.cu index 941592af75ee..0931724dfd41 100644 --- a/cpp/src/json/json_path.cu +++ b/cpp/src/json/json_path.cu @@ -367,8 +367,10 @@ class json_state : private parser { { char const quote = options.get_allow_single_quotes() ? 0 : '\"'; - auto const result = parse_string(name, can_be_empty, quote); - if (result != parse_result::SUCCESS) { return result; } + if (auto const result = parse_string(name, can_be_empty, quote); + result != parse_result::SUCCESS) { + return result; + } // a name is present, so the next char must be a : if (!parse_whitespace()) { return parse_result::ERROR; } From f61434f63809e048339a9eb9d055ea18c9ef4f5a Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Tue, 8 Sep 2026 18:19:31 +0000 Subject: [PATCH 4/5] Drop redundant comment in ObjectWithEmptyKey test --- cpp/tests/json/json_tests.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/cpp/tests/json/json_tests.cpp b/cpp/tests/json/json_tests.cpp index e2759d1f1e19..5ee3e10c8e78 100644 --- a/cpp/tests/json/json_tests.cpp +++ b/cpp/tests/json/json_tests.cpp @@ -1046,8 +1046,6 @@ TEST_F(JsonPathTests, QueriesContainingQuotes) TEST_F(JsonPathTests, ObjectWithEmptyKey) { - // A zero-length key is a legal JSON name. Scanning past it must not abort the row, so fields - // that follow an empty key have to remain reachable. auto const input = cudf::test::strings_column_wrapper{R"({"":0,"a":1})", R"({"a":1,"":0})", R"({"" : 0, "a" : 1})", From a2330eedb723176dd8db3af69ee7553daf085eaf Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Tue, 8 Sep 2026 18:32:59 +0000 Subject: [PATCH 5/5] Use canonical SPDX copyright notice in json_tests.cpp --- cpp/tests/json/json_tests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/tests/json/json_tests.cpp b/cpp/tests/json/json_tests.cpp index 5ee3e10c8e78..ab22dc1f4a93 100644 --- a/cpp/tests/json/json_tests.cpp +++ b/cpp/tests/json/json_tests.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */