0

I wrote the following code to represent JSON data in C++. I got some vague review comments that this may not be optimal and if we decide to parse JSON data directly into this structure then we might have trouble. I did not quite get the terse comments, so shall reproduce my code here and hope to hear what is wrong.

#define BOOST_VARIANT_NO_FULL_RECURSIVE_VARIANT_SUPPORT
#include <boost/variant.hpp>
#include <iostream>
#include <map>
#include <vector>
#include <algorithm>
#include <iomanip>
#define STR_(x) std::string(x)

struct JSONNullType {};

typedef boost::make_recursive_variant<
       std::string,
       long,
       double,
       std::map<std::string, boost::recursive_variant_>,
        std::vector<boost::recursive_variant_>,
       bool,
       JSONNullType>::type JSONValue;

typedef std::vector<JSONValue> JSONArray;
typedef std::map<std::string, JSONValue> JSONObject;


struct JSONPrintVisitor : public boost::static_visitor<void>
{
  void operator()(const JSONArray& array) const
  {
    std::cout << '[';

    if (!array.empty()) {
      boost::apply_visitor(*this, array[0]);

      std::for_each(array.begin() + 1, array.end(),
                  [this](const JSONValue& v) {
                    std::cout << ',';
                    boost::apply_visitor(*this, v);
                  });
    }

    std::cout << ']' << std::endl;
  }

  void operator()(const JSONObject& object) const
  {
    std::cout << '{';

    if (!object.empty()) {
      const auto& kv_pair = *(object.begin());
      std::cout << '"' << kv_pair.first << '"';
      std::cout << ':';
      boost::apply_visitor(*this, kv_pair.second);

      auto it = object.begin();
      std::for_each(++it, object.end(),
                  [this](const JSONObject::value_type& v) {
                    std::cout << ',';
                    std::cout << '"' << v.first << '"';
                    std::cout << ':';
                    boost::apply_visitor(*this, v.second);
                  });
    }

    std::cout << '}';
  }

  void operator() (const std::string& str) const
  {
    std::cout << '"' << str << '"';
  }

  void operator() (const JSONNullType&) const
  {
    std::cout << "null";
  }

  template <typename T>
  void operator()(const T& value) const
  {
    std::cout << std::boolalpha << value;
  }
};

int main()
{
  JSONValue vt = JSONArray();
  JSONArray *array = boost::get<JSONArray>(&vt);

  JSONValue person1 = JSONObject();
  JSONValue person2 = JSONObject();
  JSONValue person3 = JSONObject();
  JSONValue person4 = JSONObject();

  JSONObject *pp1 = boost::get<JSONObject>(&person1),
             *pp2 = boost::get<JSONObject>(&person2),
             *pp3 = boost::get<JSONObject>(&person3);

  (*pp1)["name"] = STR_("Baba O'Riley");
  (*pp1)["profession"] = STR_("farmer");
  (*pp1)["age"] = 21L;
  (*pp1)["favourite"] = STR_("Baba ganoush");
  (*pp1)["height"] = 176.1;

  (*pp2)["name"] = STR_("Stuart Little");
  (*pp2)["profession"] = STR_("good-boy mouse");
  (*pp2)["age"] = 4L;
  (*pp2)["favourite"] = STR_("Gouda");
  (*pp2)["height"] = 11.0;

  (*pp3)["name"] = STR_("Howard Roark");
  (*pp3)["profession"] = STR_("architect");
  (*pp3)["age"] = 32L;
  (*pp3)["favourite"] = STR_("Eggs benedict");
  (*pp3)["height"] = 185.0;

  array->push_back(person1);
  array->push_back(person2);
  array->push_back(person3);

  boost::apply_visitor(JSONPrintVisitor(), vt);
}
CppNoob
  • 2,322
  • 1
  • 24
  • 35

1 Answers1

2

I use a very similar structure with my Boost Spirit parser/generator.

It's working well.

I'd say

  • watch out for JSON that attaches meaning to order of elements in objects; your rep doesn't retain that information
  • watch out for JSON that may contain the same key twice in an object (it SHOULD not happen, but some implementations might)
  • JSON doesn't have "long" or "double". It just has "number". Make sure your long has 64 bits (uint64_t) and prepare to arbitrarily mix longs/doubles (unless you know which to expect due to some kind of schema information like Edm)
  • look out for implicit conversions leading to ambiguous constructors. It maybe worthwhile to have a strong user-defined type instead of bool, long and double. Consider what happens/should happen if you say

    JSON::Value v(42u);
    v = 7.5f;
    

An obvious optimization for some applications is to reference cached keys instead of copying strings for them. Boost Flyweight may be handy (although in my tests performance wasn't gained during parsing unless object tracking is disabled; you have to be very sure about your input sanity to allow an "ever-growing" cache because it could lead to DoS).

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Would Boost string_ref have helped instead of flyweight? – CppNoob May 27 '14 at 07:03
  • @CppNoob yes, `string_ref` performed really well. However, it has a totally different trade-off: keeping the source range around defeats storage efficiency in most scenarios, and there's a complexity cost when handling strings with escapes or character encodings. I'd say, in that sense, `string_ref` is more like "lazy parsing", and this idea could be extended a bit farther with even greater gains for Objects, Arrays. – sehe May 27 '14 at 07:23
  • [JSON objects are unordered](https://stackoverflow.com/a/7214312/2748628). From page 6 of [RFC 7159](http://www.rfc-editor.org/rfc/rfc7159.txt): **Implementations whose behavior does not depend on member ordering will be interoperable in the sense that they will not be affected by these differences.** – bit2shift Apr 22 '18 at 21:13
  • @bit2shift Implementations differ. Obviously, you're correct as to the extent of the spec. JSON is a minefield: http://seriot.ch/parsing_json.php – sehe Apr 22 '18 at 21:20