JSON Voorhees: Overview (original) (raw)
JSON Voorhees is a JSON library written for the C++ programmer who wants to be productive in this modern world. What does that mean? There are a ton of JSON libraries floating around touting how they are "modern" C++ and so on. But who really cares? JSON Voorhees puts the focus more on the resulting C++ than any "modern" feature set. This means the library does not skip on string encoding details like having full support for UTF-8. Are there "modern" features? Sure, but this library is not meant to be a gallery of them – a good API should get out of your way and let you work. It is hosted on GitHub and sports an Apache License, so use it anywhere you need.
Features include (but are not necessarily limited to):
- Simple
- A
valueshould not feel terribly different from a C++ Standard Library container - Write valid JSON with
operator<< - Simple JSON parsing with
parse - Reasonable error messages when parsing fails
- Full support for Unicode-filled JSON (encoded in UTF-8 in C++)
- A
- Efficient
- Minimal overhead to store values (a
valueis 16 bytes on a 64-bit platform) - No-throw move semantics wherever possible
- Minimal overhead to store values (a
- Easy
- Convert a
valueinto a C++ type usingextract<T> - Encode a C++ type into a value using
to_json
- Convert a
- Safe
- In the best case, illegal code should fail to compile
- An illegal action should throw an exception
- Almost all utility functions have a strong exception guarantee.
- Stable
- Worry less about upgrading – the API and ABI will not change out from under you
- Documented
- Consumable by human beings
- Answers questions you might actually ask
JSON Voorhees is designed with ease-of-use in mind. So let's look at some code!
The jsonv::value
The central class of JSON Voorhees is the [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff..."). This class represents a JSON AST and is somewhat of a dynamic type. This can make things a little bit awkward for C++ programmers who are used to static typing. Don't worry about it – you can learn to love it.
Putting values of different types is super-easy.
#include
int main()
{
std::cout << x << std::endl;
x = 5.9;
std::cout << x << std::endl;
x = -100;
std::cout << x << std::endl;
x = "something else";
std::cout << x << std::endl;
x = jsonv::array({ "arrays", "of", "the", 7, "different", "types"});
std::cout << x << std::endl;
"Are fun, too.",
"Do what you want."
})
},
{ "compose like", "standard library maps" },
});
std::cout << x << std::endl;
}
Output:
5.9
-100
"something else"
["arrays","of","the",7,"different","types"]
{"compose like":"standard library maps","objects":["Are fun, too.","Do what you want."]}
If that isn't convenient enough for you, there is a user-defined literal _json in the jsonv namespace you can use
using jsonv::operator"" _json;
"objects": [ "Are fun, too.",
"Do what you want."
],
"compose like": "You are just writing JSON",
"which I guess": ["is", "also", "neat"]
})"_json;
JSON is dynamic, which makes value access a bit more of a hassle, but JSON Voorhees aims to make it not too horrifying for you. A [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...") has a number of accessor methods named things like as_integer and as_string which let you access the value as if it was that type. But what if it isn't that type? In that case, the function will throw a [jsonv::kind_error](classjsonv%5F1%5F1kind%5F%5Ferror.html "Thrown from various value methods when attempting to perform an operation which is not valid for the ...") with a bit more information as to what rule you violated.
#include
int main()
{
try
{
}
{
std::cout << err.what() << std::endl;
}
x = "now make it a string";
std::cout << x.as_string().size() << std::endl;
std::cout << x.as_string() << "\tis not the same as\t" << x << std::endl;
}
Output:
Unexpected type: expected string but found null.
20
now make it a string is not the same as "now make it a string"
You can also deal with container types in a similar manner that you would deal with the equivalent STL container type, with some minor caveats. Because the value_type of a JSON object and JSON array are different, they have different iterator types in JSON Voorhees. They are aptly-named object_iterator and array_iterator. The access methods for these iterators are begin_object / end_object and begin_array / end_array, respectively. The object interface behaves exactly like you would expect a std::map<std::string,jsonv::value> to, while the array interface behaves just like a std::deque<jsonv::value> would.
#include
int main()
{
auto iter = x.find("one");
std::cout << iter->first << ": " << iter->second << std::endl;
else
std::cout << "Nothing..." << std::end;
iter = x.find("two");
std::cout << iter->first << ": " << iter->second << std::endl;
else
std::cout << "Nothing..." << std::end;
x["two"] = 2;
iter = x.find("two");
std::cout << iter->first << ": " << iter->second << std::endl;
else
std::cout << "Nothing..." << std::end;
x["two"] = jsonv::array({ "one", "+", x.at("one") });
iter = x.find("two");
std::cout << iter->first << ": " << iter->second << std::endl;
else
std::cout << "Nothing..." << std::end;
iter = x.find("one");
std::cout << iter->first << ": " << iter->second << std::endl;
else
std::cout << "Nothing..." << std::end;
}
Output:
one: 1
Nothing...
two: 2
two: ["one","+",1]
Nothing...
The iterator types work. This means you are free to use all of the C++ things just like you would a regular container. To use a ranged-based for, simply call as_array or as_object. Everything from <algorithm> and or any other library works great with JSON Voorhees. Bring those templates on!
#include
#include
int main()
{
std::cout << "Initial: ";
for (const auto& val : arr.as_array())
std::cout << val << '\t';
std::cout << std::endl;
std::cout << "Sorted: ";
for (const auto& val : arr.as_array())
std::cout << val << '\t';
std::cout << std::endl;
}
Output:
Initial: "taco" "cat" 3 -2 null "beef" 4.8 5
Sorted: null -2 3 4.8 5 "beef" "cat" "taco"
Encoding and decoding
Usually, the reason people are using JSON is as a data exchange format, either for communicating with other services or storing things in a file or a database. To do this, you need to encode your json::value into an std::string and parse it back. JSON Voorhees makes this very easy for you.
#include
#include
#include
int main()
{
obj["taco"] = "cat";
obj["infinity"] = std::numeric_limits::infinity();
{
std::cout << "Saving \"file.json\"... " << obj << std::endl;
std::ofstream file("file.json");
file << obj;
}
{
std::cout << "Loading \"file.json\"...";
std::ifstream file("file.json");
loaded = jsonv::parse(file);
}
std::cout << loaded << std::endl;
return obj == loaded ? 0 : 1;
}
Output:
Saving "file.json"... {"array":[1,2,3,4,5],"infinity":null,"taco":"cat"}
Loading "file.json"...{"array":[1,2,3,4,5],"infinity":null,"taco":"cat"}
If you are paying close attention, you might have noticed that the value for the "infinity" looks a little bit more null than infinity. This is because, much like mathematicians before Anaximander, JSON has no concept of infinity, so it is actually illegal to serialize a token like infinity anywhere. By default, when an encoder encounters an unrepresentable value in the JSON it is trying to encode, it outputs null instead. If you wish to change this behavior, implement your own [jsonv::encoder](classjsonv%5F1%5F1encoder.html "An encoder is responsible for writing values to some form of output. ") (or derive from [jsonv::ostream_encoder](classjsonv%5F1%5F1ostream%5F%5Fencoder.html "An encoder that outputs to an std::ostream. ")). If you ran the example program, you might have noticed that the return code was 1, meaning the value you put into the file and what you got from it were not equal. This is because all the type and value information is still kept around in the in-memory obj. It is only upon encoding that information is lost.
Getting tired of all this compact rendering of your JSON strings? Want a little more whitespace in your life? Then [jsonv::ostream_pretty_encoder](classjsonv%5F1%5F1ostream%5F%5Fpretty%5F%5Fencoder.html) is the class for you! Unlike our standard compact encoder, this guy will put newlines and indentation in your JSON so you can present it in a way more readable format.
#include
int main()
{
prettifier.encode(jsonv::parse(std::cin));
}
Compile that code and you now have your own little JSON prettification program!
Serialization
Most of the time, you do not want to deal with [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...") instances directly. Instead, most people prefer to convert [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...") instances into their own strong C++ class or struct. JSON Voorhees provides utilities to make this easy for you to use. At the end of the day, you should be able to create an arbitrary C++ type with jsonv::extract<my_type>(value) and create a [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...") from your arbitrary C++ type with jsonv::to_json(my_instance).
Extracting with extract
Let's start with converting a [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...") into a custom C++ type with jsonv::extract<T>.
#include
int main()
{
jsonv::value val = jsonv::parse(R"({ "a": 1, "b": 2, "c": "Hello!" })");
std::cout << "a=" << jsonv::extract(val.at("a")) << std::endl;
std::cout << "b=" << jsonv::extract(val.at("b")) << std::endl;
std::cout << "c=" << jsonv::extractstd::string(val.at("c")) << std::endl;
}
Output:
Overall, this is not very complicated. We did not do anything that could not have been done through a little use of as_integer and as_string. So what is this extract giving us?
The real power comes in when we start talking about [jsonv::formats](classjsonv%5F1%5F1formats.html "Simply put, this class is a collection of extractor and serializer instances. "). These objects provide a set of rules to encode and decode arbitrary types. So let's make a C++ class for our JSON object and write a special constructor for it.
#include
class my_type
{
public:
a(context.extract_sub(from, "a")),
b(context.extract_sub(from, "b")),
c(context.extract_sub<std::string>(from, "c"))
{ }
{
return &instance;
}
friend std::ostream& operator<<(std::ostream& os, const my_type& self)
{
return os << "{ a=" << self.a << ", b=" << self.b << ", c=" << self.c << " }";
}
private:
int a;
int b;
std::string c;
};
int main()
{
jsonv::value val = jsonv::parse(R"({ "a": 1, "b": 2, "c": "Hello!" })");
my_type x = jsonv::extract<my_type>(val, format);
std::ostream << x << std::endl;
}
Output:
There is a lot going on in that example, so let's take it one step at a time. First, we are creating a my_type object to store our values, which is nice. Then, we gave it a funny-looking constructor:
a(context.extract_sub(from, "a")),
b(context.extract_sub(from, "b")),
c(context.extract_sub<std::string>(from, "c"))
{ }
This is an extracting constructor. All that means is that it has those two arguments: a [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...") and a [jsonv::extraction_context](classjsonv%5F1%5F1extraction%5F%5Fcontext.html). The [jsonv::extraction_context](classjsonv%5F1%5F1extraction%5F%5Fcontext.html) is an optional, but extremely helpful class. Inside the constructor, we use the [jsonv::extraction_context](classjsonv%5F1%5F1extraction%5F%5Fcontext.html) to access the values of the incoming JSON object in order to build our object.
A [jsonv::extractor](classjsonv%5F1%5F1extractor.html "An extractor holds the method for converting a value into an arbitrary C++ type. ") is a type that knows how to take a [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...") and create some C++ type out of it. In this case, we are creating a [jsonv::extractor_construction](classjsonv%5F1%5F1extractor%5F%5Fconstruction.html), which is a subtype that knows how to call the constructor of a type. There are all sorts of [jsonv::extractor](classjsonv%5F1%5F1extractor.html "An extractor holds the method for converting a value into an arbitrary C++ type. ") implementations in [jsonv/serialization.hpp](serialization%5F8hpp.html "Conversion between C++ types and JSON values. "), so you should be able to find one that fits your needs.
Now things are starting to get interesting. The [jsonv::formats](classjsonv%5F1%5F1formats.html "Simply put, this class is a collection of extractor and serializer instances. ") object is a collection of [jsonv::extractor](classjsonv%5F1%5F1extractor.html "An extractor holds the method for converting a value into an arbitrary C++ type. ")s, so we create one of our own and add the [jsonv::extractor](classjsonv%5F1%5F1extractor.html "An extractor holds the method for converting a value into an arbitrary C++ type. ")* from the static function of my_type. Now, local_formats only knows how to extract instances of my_type – it does not know even the most basic things like how to extract an int. We use [jsonv::formats::compose](classjsonv%5F1%5F1formats.html#ae8fdc86436f8dcfd405753db55620110 "Create a new (empty) formats using the bases as backing formats. ") to create a new instance of [jsonv::formats](classjsonv%5F1%5F1formats.html "Simply put, this class is a collection of extractor and serializer instances. ") that combines the qualities of local_formats (which knows how to deal with my_type) and the [jsonv::formats::defaults](classjsonv%5F1%5F1formats.html#ad194791e293229fb6a79438a6444f51b "Get the default formats instance. ") (which knows how to deal with things like int and std::string). The formats instance now has the power to do everything we need!
my_type x = jsonv::extract<my_type>(val, format);
This is not terribly different from the example before, but now we are explicitly passing a [jsonv::formats](classjsonv%5F1%5F1formats.html "Simply put, this class is a collection of extractor and serializer instances. ") object to the function. If we had not provided format as an argument here, the function would have thrown a [jsonv::extraction_error](classjsonv%5F1%5F1extraction%5F%5Ferror.html "Exception thrown if there is any problem running extract. ") complaining about how it did not know how to extract a my_type.
Serialization with to_json
JSON Voorhees also allows you to convert from your C++ structures into JSON values, using jsonv::to_json. It should feel like a mirror jsonv::extract, with similar argument types and many shared concepts. Just like extraction, jsonv::to_json uses the [jsonv::formats](classjsonv%5F1%5F1formats.html "Simply put, this class is a collection of extractor and serializer instances. ") class, but it uses a [jsonv::serializer](classjsonv%5F1%5F1serializer.html "A serializer holds the method for converting an arbitrary C++ type into a value. ") to convert from C++ into JSON.
#include
class my_type
{
public:
my_type(int a, int b, std::string c) :
a(a),
b(b),
c(std::move(c))
{ }
{
static auto instance = jsonv::make_serializer<my_type>
(
{
return jsonv::object({ { "a", context.to_json(self.a) },
{ "b", context.to_json(self.b) },
{ "c", context.to_json(self.c) }
}
);
}
);
return &instance;
}
private:
int a;
int b;
std::string c;
};
int main()
{
my_type x(5, 6, "Hello");
}
Output:
{"a":5,"b":6,"c":"Hello"}
Composing Type Adapters
Does all this seem a little bit manual to you? Creating an extractor and serializer for every single type can get a little bit tedious. Unfortunately, until C++ has a standard way to do reflection, we must specify the conversions manually. However, there is an easier way! That way is the Serialization Builder DSL.
Let's start with a couple of simple structures:
struct foo
{
int a;
int b;
std::string c;
};
struct bar
{
foo x;
foo y;
std::string z;
std::string w;
};
Let's make a formats for them using the DSL:
.type()
.member("a", &foo::a)
.member("b", &foo::b)
.default_value(10)
.member("c", &foo::c)
.type()
.member("x", &bar::x)
.member("y", &bar::y)
.member("z", &bar::z)
.member("w", &bar::w)
;
What is going on there? The giant chain of function calls is building up a collection of type adapters into a formats for you. The indentation shows the intent – the .member("a", &foo::a) is attached to the type adapter for foo (if you tried to specify &bar::y in that same place, it would fail to compile). Each function call returns a reference back to the builder so you can chain as many of these together as you want to. The [jsonv::formats_builder](classjsonv%5F1%5F1formats%5F%5Fbuilder.html) is a proper object, so if you wish to spread out building your type adapters into multiple functions, you can do that by passing around an instance.
The two most-used functions are type and member. type defines a [jsonv::adapter](classjsonv%5F1%5F1adapter.html "An adapter is both an extractor and a serializer. ") for the C++ class provided at the template parameter. All of the calls before the second type call modify the adapter for foo. There, we attach members with the member function. This tells the formats how to encode and extract each of the specified members to and from a JSON object using the provided string as the key. The extra function calls like default_value, since and until are just a could of the many functions available to modify how the members of the type get transformed.
The formats we built would be perfectly capable of serializing to and extracting from this JSON document:
{
"x": { "a": 50, "b": 20, "c": "Blah" },
"y": { "a": 10, "c": "No B?" },
"z": "Only serialized in 2.0+",
"w": "Only serialized before 5.0"
}
For a more in-depth reference, see the Serialization Builder DSL page.
Algorithms
JSON Voorhees takes a "batteries included" approach. A few building blocks for powerful operations can be found in the [algorithm.hpp](algorithm%5F8hpp.html "A collection of algorithms a la <algorithm>. ") header file.
One of the simplest operations you can perform is the map operation. This operation takes in some [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...") and returns another. Let's try it.
#include
int main()
{
}
If everything went right, you should see a number:
Okay, so that was not very interesting. To be fair, that is not the most interesting example of using map, but it is enough to get the general idea of what is going on. This operation is so common that it is a member function of value as [jsonv::value::map](group%5F%5FAlgorithm.html#ga9e2e68ce801ca3c29f80f104dd12c99d "Run a function over the values of this instance. "). Let's make things a bit more interesting and map an array...
#include
int main()
{
<< std::endl;
}
Now we're starting to get somewhere!
The map function maps over whatever the contents of the [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...") happens to be and returns something for you based on the kind. This simple concept is so ubiquitous that Eugenio Moggi named it a monad. If you're feeling adventurous, try using map with an object or chaining multiple map operations together.
Another common building block is the function jsonv::traverse. This function walks a JSON structure and calls a some user-provided function.
#include
int main()
{
{
std::cout << path << "=" << value << std::endl;
},
true
);
}
Now we have a tiny little program! Here's what happens when I pipe { "bar": [1, 2, 3], "foo": "hello" } into the program:
.bar[0]=1
.bar[1]=2
.bar[2]=3
.foo="hello"
Imagine the possibilities!
All of the really powerful functions can be found in [util.hpp](util%5F8hpp.html "Deprecated. "). My personal favorite is jsonv::merge. The idea is simple: it merges two (or more) JSON values into one.
#include
int main()
{
std::cout << merged << std::endl;
}
Output:
{"a":"taco","b":"cat","c":"burrito","d":"dog"}
You might have noticed the use of std::move into the merge function. Like most functions in JSON Voorhees, merge takes advantage of move semantics. In this case, the implementation will move the contents of the values instead of copying them around. While it may not matter in this simple case, if you have large JSON structures, the support for movement will save you a ton of memory.
See also