Search code examples
c++jsonboostboost-propertytree

How to convert any value to an object and add members with boost::property_tree json


I have a program that modifies a JSON document if necessary. The program has to add a child to another value whether or not it's an already an object. The program should behave like so:

  1. If the object with key "x" does not exist, create object with key "x" and add value y as a child.
  2. If the object with key "x" DOES exist, set value y as a child.
  3. If the key "x" exists and is ANY OTHER type, delete it, create an object with the key "x" and then add value y as a child.

I see ways to test if property tree values exist or whether they are specified types, but none to test if it's an object or not an object.

Here's a simple program I made illustrating what I mean:

#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>

#include <sstream>
#include <iostream>

const char *json = "{"
"\"object\" : { \"mighty\" : \"wind\" },"
"\"boolean\" : true"
"}";

void printTree( std::string name, boost::property_tree::ptree tree )
{
    std::cout << "Pass '" << name << "'" << std::endl;
    try
    {
        std::stringstream ss;
        boost::property_tree::write_json( ss, tree );
        std::cout << ss.str() << std::endl;
    }
    catch( std::exception &e )
    {
        std::cout << "Could not make create json: " << e.what() << std::endl;
    }
}

int main( int argc, char *argv[] )
{
    boost::property_tree::ptree tree;

    // Load it
    std::istringstream ss_json( json );
    boost::property_tree::read_json( ss_json, tree );

    // Add a value to an object that doesn't exist
    tree.put( "none.value", "hello!" );

    // Print to see
    printTree( "Nonexistent value test", tree );

    // Add a value to the object
    tree.put( "object.value", "bello!" );

    // Print this one
    printTree( "Adding value test", tree );

    // Convert boolean to an object and add a value
    tree.put( "boolean.value", "mello!" );

    // Print it
    printTree( "Converting value test", tree );
}

The output will be:

Pass 'Nonexistent value test'
{
    "object": {
        "mighty": "wind"
    },
    "boolean": "true",
    "none": {
        "value": "hello!"
    }
}

Pass 'Adding value test'
{
    "object": {
        "mighty": "wind",
        "value": "bello!"
    },
    "boolean": "true",
    "none": {
        "value": "hello!"
    }
}

Pass 'Converting value test'
Could not make create json: <unspecified file>: ptree contains data that cannot be represented in JSON format

You can see in the output, the last step fails to convert to JSON (doesn't throw when I try to set it).

How can I achieve scenario 3 in my list above?


Solution

  • If the key "x" exists and is ANY OTHER type, delete it, create an object with the key "x" and then add value y as a child. Also, they don't observe any of the JSON data types.

    Your plan is pretty doomed. Property Tree is not a JSON library. Property Trees can have data and child nodes at the same node. E.g.

    ptree p;
    auto& x = p.put_child("x", {});
    x.put_value("hello");
    
    write_json(std::cout, p);
    

    Prints

    {
        "x": "hello"
    }
    

    But adding

    /*auto& a = */ p.put_child("x.a", {});
    write_json(std::cout, p);
    

    Fails with Live On Coliru

    terminate called after throwing an instance of 'boost::wrapexcept<boost::property_tree::json_parser::json_parser_error>'
      what():  <unspecified file>: ptree contains data that cannot be represented in JSON format
    

    A workaround would be to remove any value prior to or when adding properties:

    x.put_value("");
    auto& a = p.put_child("x.a", {});
    a.add("prop1", 123);
    a.add("prop2", "one two three");
    a.add("b.prop1", "nesting");
    write_json(std::cout, p);
    

    Would print Live On Coliru

    Finer notes

    It might seem more efficient to check the presence of a value before clearing it:

    if (x.get_value_optional<std::string>()) {
        x.put_value("");
    }
    

    But due the the stringly typed nature of Property Tree storage there's no difference as the condition will just always be true for std::string. (Similarly there's no way to retrieve a value by reference.)

    Note ALSO that when setting the n.prop1 nested property, you MAY have to also check that b has no value if you don't control the source data, because otherwise it would fail again.

    Assuming that your object graph structure is reasonably predictable (or even static), I'd suggest getting it over with ahead of time:

    for (auto key : { "x", "x.a", "x.a.b" }) {
        if (auto child = p.get_child_optional(key)) {
            std::cout << "clearing " << key << std::endl;
            child->put_value("");
        }
    }
    

    Which can be generalized with a helper:

    clear_values("x.a.b", p);
    

    Which could be implemented as

    void clear_values(ptree::path_type path, ptree& p) {
        if (path.empty())
            return;
    
        auto head = path.reduce();
    
        auto child = p.get_child_optional(head);
        if (child) {
            child->put_value("");
            clear_values(path, *child);
        }
    }
    

    Bonus

    In fact with such a helper it might become opportune to also create the expected hierarchy on the fly:

    void clear_values(ptree::path_type path, ptree& p, bool create = false) {
        if (path.empty())
            return;
    
        auto head = path.reduce();
    
        auto child = p.get_child_optional(head);
        if (!child && create) {
            child = p.put_child(head, {});
        }
    
        if (child) {
            child->put_value("");
            clear_values(path, *child, create);
        }
    }
    

    Now it would even work well without any pre-existing data:

    Live On Coliru

    #include <boost/property_tree/json_parser.hpp>
    #include <iostream>
    
    using boost::property_tree::ptree;
    
    void clear_values(ptree::path_type path, ptree& p, bool create = false) {
        if (path.empty())
            return;
    
        auto head = path.reduce();
    
        auto child = p.get_child_optional(head);
        if (!child && create) {
            child = p.put_child(head, {});
        }
    
        if (child) {
            child->put_value("");
            clear_values(path, *child, create);
        }
    }
    
    int main() {
        ptree p;
        clear_values("x.a.b", p, true);
    
        auto& a = p.get_child("x.a");
        a.add("prop1", 123);
        a.add("prop2", "one two three");
        a.add("b.prop1", "nesting");
        write_json(std::cout, p);
    }
    

    Prints

    {
        "x": {
            "a": {
                "b": {
                    "prop1": "nesting"
                },
                "prop1": "123",
                "prop2": "one two three"
            }
        }
    }