danielaparker / jsoncons

A C++, header-only library for constructing JSON and JSON-like data formats, with JSON Pointer, JSON Patch, JSON Schema, JSONPath, JMESPath, CSV, MessagePack, CBOR, BSON, UBJSON
https://danielaparker.github.io/jsoncons
Other
726 stars 164 forks source link

Retrieve the parent of an object located by jsonpath #467

Closed PragmaTwice closed 1 year ago

PragmaTwice commented 1 year ago

Describe the proposed feature Hi @danielaparker, firstly I want to thank you for such an amazing JSON library with rich features.

Our Kvrocks community is utilizing this library to implement JSON-related features like RedisJSON-compatibility. And my experience with jsoncons is really awesome.

For implementing some commands like JSON.DEL, we need to firstly use the jsonpath extension to locate an object in a JSON document, and then we need to delete the object if it is inside an array or object. (For example, given a JSON {"a":1, "b":2}, JSON.DEL <key> $.a results in {"b":2}.)

But it seems to be hard to delete since we cannot retrieve the parent object (or array) and then use some methods like basic_json::erase. (For example, given a JSON {"a":1, "b":2} and jsonpath $.a, we can only get the object 1, but instead we need to retrieve its parent object and then delete the key a.)

So I am wondering if there is any chance that such an ability can be provided in jsoncons, or if there is an cheap workaround which can solve this problem.

Related issue in Kvrocks: https://github.com/apache/kvrocks/issues/1813

What other libraries (C++ or other) have this feature? Maybe RedisJSON? Since it implements JSON.DEL.

danielaparker commented 1 year ago

jsoncons jsonpath supports a parent selector extension operator, ^, borrowed from jsonpath-plus, that should do what you want.

#include <jsoncons/json.hpp>
#include <jsoncons_ext/jsonpath/jsonpath.hpp>

using namespace jsoncons;

std::string input = R"(
[{"a":1, "b":2},{"c":1,"d":2}]
)";

int main()
{
    json doc = json::parse(input);

    auto callback = [](const std::string& path, json& value)
    {
        if (value.is_object())
        {
            auto it = value.find("a");
            value.erase(it);
        }
    };
    jsonpath::json_replace(doc, "$..a^", callback);

    std::cout << doc << "\n";
}

Output:

[{"b":2},{"c":1,"d":2}]
PragmaTwice commented 1 year ago

I see. Thank you for your quick answer!

PragmaTwice commented 1 year ago

The parent selector is awesome, but we still face some problem to retrieve the key name.

For the example in https://github.com/danielaparker/jsoncons/issues/467#issuecomment-1783967621, it seems to be hard to retrieve the field name a in the parent object, since the path can be varied (e.g. $.a, $["a"], $[1], $..a[*]).

May I ask if you have some idea about this? Thanks.

danielaparker commented 1 year ago

Oh, I see. In that case I think the strategy would be to collect the normalized paths for all nodes associated with the path, and then delete them one by one. We don't currently have a utility function for deleting the node corresponding to a normalized path, but could add one in the next release.

PragmaTwice commented 1 year ago

We don't currently have a utility function for deleting the node corresponding to a normalized path, but could add one in the next release.

Thank you. It will be highly appreciated.

danielaparker commented 1 year ago

Release 0.172.0 supports removing nodes that match on a JSONPath expression, e.g.

#include <jsoncons/json.hpp>
#include <jsoncons_ext/jsonpath/jsonpath.hpp>
#include <fstream>
#include <cassert>

using jsoncons::json; 
namespace jsonpath = jsoncons::jsonpath;

int main()
{
    std::string input = R"(
    {
        "books":
        [
            {
                "category": "fiction",
                "title" : "A Wild Sheep Chase",
                "author" : "Haruki Murakami",
                "price" : 22.72
            },
            {
                "category": "fiction",
                "title" : "The Night Watch",
                "author" : "Sergei Lukyanenko",
                "price" : 23.58
            },
            {
                "category": "fiction",
                "title" : "The Comedians",
                "author" : "Graham Greene",
                "price" : 21.99
            },
            {
                "category": "memoir",
                "title" : "The Night Watch",
                "author" : "Phillips, David Atlee"
            }
        ]
    }
    )";
    json doc = json::parse(input);

    std::size_t n = jsonpath::remove(doc, "$.books[1,1,3,3,0,0]");

    assert(n == 3);  // Number of nodes removed

    std::cout << jsoncons::pretty_print(doc) << "\n\n";

Output:

{
    "books": [
        {
            "author": "Graham Greene",
            "category": "fiction",
            "price": 21.99,
            "title": "The Comedians"
        }
    ]
}

Before removal, duplicate nodes are removed, and locations to remove are sorted in descending path order, so e.g. node "$['books'][3]" is removed once, and node "$['books'][3]" is removed before nodes "$['books'][1]" and "$['books'][0]".

PragmaTwice commented 12 months ago

Thank you!