JSON Voorhees
Killer JSON for C++
|
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):
value
should not feel terribly different from a C++ Standard Library containeroperator<<
parse
value
is 16 bytes on a 64-bit platform)value
into a C++ type using extract<T>
to_json
JSON Voorhees is designed with ease-of-use in mind. So let's look at some code!
The central class of JSON Voorhees is the jsonv::value
. 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.
Output:
If that isn't convenient enough for you, there is a user-defined literal _json
in the jsonv
namespace you can use
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
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
with a bit more information as to what rule you violated.
Output:
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.
Output:
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
<iterator> or any other library works great with JSON Voorhees. Bring those templates on!
Output:
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.
Output:
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
(or derive from jsonv::ostream_encoder
). 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
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.
Compile that code and you now have your own little JSON prettification program!
Most of the time, you do not want to deal with jsonv::value
instances directly. Instead, most people prefer to convert jsonv::value
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
from your arbitrary C++ type with jsonv::to_json(my_instance)
.
Let's start with converting a jsonv::value
into a custom C++ type with jsonv::extract<T>
.
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
. 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.
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:
This is an extracting constructor. All that means is that it has those two arguments: a jsonv::value
and a jsonv::extraction_context
. The jsonv::extraction_context
is an optional, but extremely helpful class. Inside the constructor, we use the jsonv::extraction_context
to access the values of the incoming JSON object in order to build our object.
A jsonv::extractor
is a type that knows how to take a jsonv::value
and create some C++ type out of it. In this case, we are creating a jsonv::extractor_construction
, which is a subtype that knows how to call the constructor of a type. There are all sorts of jsonv::extractor
implementations in jsonv/serialization.hpp
, so you should be able to find one that fits your needs.
Now things are starting to get interesting. The jsonv::formats
object is a collection of jsonv::extractor
s, so we create one of our own and add the jsonv::extractor*
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
to create a new instance of jsonv::formats
that combines the qualities of local_formats
(which knows how to deal with my_type
) and the jsonv::formats::defaults
(which knows how to deal with things like int
and std::string
). The formats
instance now has the power to do everything we need!
This is not terribly different from the example before, but now we are explicitly passing a jsonv::formats
object to the function. If we had not provided format
as an argument here, the function would have thrown a jsonv::extraction_error
complaining about how it did not know how to extract a my_type
.
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
class, but it uses a jsonv::serializer
to convert from C++ into JSON.
Output:
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:
Let's make a formats
for them using the DSL:
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
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
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:
For a more in-depth reference, see the Serialization Builder DSL page.
JSON Voorhees takes a "batteries included" approach. A few building blocks for powerful operations can be found in the algorithm.hpp
header file.
One of the simplest operations you can perform is the map
operation. This operation takes in some jsonv::value
and returns another. Let's try it.
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
. Let's make things a bit more interesting and map
an array
...
Now we're starting to get somewhere!
The map
function maps over whatever the contents of the jsonv::value
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.
Now we have a tiny little program! Here's what happens when I pipe { "bar": [1, 2, 3], "foo": "hello" }
into the program:
Imagine the possibilities!
All of the really powerful functions can be found in util.hpp
. My personal favorite is jsonv::merge
. The idea is simple: it merges two (or more) JSON values into one.
Output:
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.