JSON Voorhees
Killer JSON for C++
Loading...
Searching...
No Matches
Overview

JSON Voorhees is a JSON library written for the C++ programmer who wants to be productive in this modern world. This one targets C++23 for developer-friendliness, a reasonably fast parser, and no dependencies beyond a compliant compiler and standard library. 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 value should 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++)
  • Efficient
    • Minimal overhead to store values (a value is 16 bytes on a 64-bit platform)
    • No-throw move semantics wherever possible
  • Serialization/Deserialization
    • Deserialize a C++ type straight from JSON text, or from a value, using deserialize<T>
    • Serialize a C++ type straight to JSON text using serialize, or into a value using to_json
  • Safe
    • In the best case, illegal code should fail to compile
    • An illegal action should throw an exception
    • The query API is [[nodiscard]], so dropping the answer to a question you asked is a warning
    • 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, which represents a JSON AST. Putting values of different types is easy.

#include <jsonv/value.hpp>
#include <iostream>
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?", true });
std::cout << x << std::endl;
{ "objects", jsonv::array({
"Are fun, too.",
"Do what you want."
})
},
{ "compose like", "standard library maps" },
});
std::cout << x << std::endl;
}
Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...
Definition value.hpp:113
JSONV_PUBLIC value object()
Create an empty object.
JSONV_PUBLIC const value null
An instance with kind::null.
JSONV_PUBLIC value array()
Create an empty array value.
Copyright (c) 2012-2020 by Travis Gockel.

Output:

5.9
-100
"something else"
["arrays","of","the",7,"different","types?",true]
{"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:

// You can use this hideous syntax if you do not want to bring in the whole jsonv namespace:
using jsonv::operator""_json;
jsonv::value x = R"({
"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 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.

#include <jsonv/value.hpp>
#include <iostream>
int main()
{
try
{
x.as_string();
}
catch (const jsonv::kind_error& err)
{
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;
}
Thrown from various value methods when attempting to perform an operation which is not valid for the ...
Definition value.hpp:76
const std::string & as_string() const
Get this value as a string.

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 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 <jsonv/value.hpp>
#include <iostream>
int main()
{
jsonv::value x = jsonv::object({ { "one", 1 }});
auto iter = x.find("one");
if (iter != x.end_object())
std::cout << iter->first << ": " << iter->second << std::endl;
else
std::cout << "Nothing..." << std::end;
iter = x.find("two");
if (iter != x.end_object())
std::cout << iter->first << ": " << iter->second << std::endl;
else
std::cout << "Nothing..." << std::end;
x["two"] = 2;
iter = x.find("two");
if (iter != x.end_object())
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");
if (iter != x.end_object())
std::cout << iter->first << ": " << iter->second << std::endl;
else
std::cout << "Nothing..." << std::end;
x.erase("one");
iter = x.find("one");
if (iter != x.end_object())
std::cout << iter->first << ": " << iter->second << std::endl;
else
std::cout << "Nothing..." << std::end;
}
object_iterator end_object()
Get an iterator to the one past the end of this object.
object_iterator find(const std::string &key)
Attempt to locate a key-value pair with the provided key in this object.
array_iterator erase(const_array_iterator position)
Erase the item at this array's position.
value & at(size_type idx)
Get the value in this array at the given idx.

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 <iterator> or any other library works great with JSON Voorhees.

#include <jsonv/value.hpp>
#include <algorithm>
#include <iostream>
int main()
{
jsonv::value arr = jsonv::array({ "taco", "cat", 3, -2, jsonv::null, "beef", 4.8, 5 });
std::cout << "Initial: ";
for (const auto& val : arr.as_array())
std::cout << val << '\t';
std::cout << std::endl;
std::sort(arr.begin_array(), arr.end_array());
std::cout << "Sorted: ";
for (const auto& val : arr.as_array())
std::cout << val << '\t';
std::cout << std::endl;
}
array_iterator begin_array()
Get an iterator to the beginning of this array.
array_iterator end_array()
Get an iterator to the end of this array.
STL namespace.

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 easy for you.

#include <jsonv/value.hpp>
#include <jsonv/encode.hpp>
#include <jsonv/parse.hpp>
#include <iostream>
#include <fstream>
#include <limits>
int main()
{
obj["taco"] = "cat";
obj["array"] = jsonv::array({ 1, 2, 3, 4, 5 });
obj["infinity"] = std::numeric_limits<double>::infinity();
{
std::cout << "Saving \"file.json\"... " << obj << std::endl;
std::ofstream file("file.json");
file << obj;
}
jsonv::value loaded;
{
std::cout << "Loading \"file.json\"...";
std::ifstream file("file.json");
loaded = jsonv::parse(file);
}
std::cout << loaded << std::endl;
return obj == loaded ? 0 : 1;
}
Classes and functions for encoding JSON values to various representations.
Copyright (c) 2012-2020 by Travis Gockel.
JSONV_PUBLIC value parse(std::string_view input, const parse_options &parse_options, const deserialize_options &deserialize_options)
Construct a JSON value from the given input.

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 (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.

#include <jsonv/encode.hpp>
#include <jsonv/parse.hpp>
#include <jsonv/value.hpp>
#include <iostream>
int main()
{
// Make a pretty encoder and point to std::cout
jsonv::ostream_pretty_encoder prettifier(std::cout);
prettifier.encode(jsonv::parse(std::cin));
}
Like ostream_encoder, but pretty prints output to an std::ostream.
Definition encode.hpp:243

Compile that code and you now have your own little JSON prettification program!

Not everything you want to write starts out as a jsonv::value. A jsonv::writer writes a document one token at a time into any encoder, the pretty one included, and jsonv::serialize writes a C++ object as one value wherever the writer is:

#include <jsonv/encode.hpp>
#include <jsonv/writer.hpp>
#include <iostream>
#include <string>
#include <vector>
int main()
{
std::vector<std::string> villains = { "Jason", "Freddy", "Michael" };
jsonv::ostream_pretty_encoder prettifier(std::cout);
jsonv::writer to(prettifier);
to.object_begin();
to.key("genre").string("slasher");
to.key("villains").array_begin();
for (const std::string& name : villains)
jsonv::serialize(name, to);
to.array_end();
to.object_end();
}
A writer instance writes a JSON ast_node sequence to some form of sink: an encoder,...
Definition writer.hpp:68
std::string serialize(const T &from, const formats &fmts=formats::global())
Serialize from into JSON text using fmts (by default jsonv::formats::global()).
Conversion between C++ types and JSON values.
Write a JSON AST.

Output:

{
"genre": "slasher",
"villains": [
"Jason",
"Freddy",
"Michael"
]
}

The writer writes the commas and the colons, and refuses a token which does not belong where it is – a key outside an object, say – before the encoder ever sees it. Each call to serialize writes one more element into the array the writer has open, so the same loop could write a million names without ever holding them all in a jsonv::value. serialize works for any type a jsonv::formats knows – here, jsonv::formats::global() – and teaching one about your own types is what the next section is about.

Serialization

Most of the time, you do not want to deal with jsonv::value instances directly. Instead, most people prefer to convert JSON 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::deserialize<my_type>(text) and turn one back into JSON text with jsonv::serialize(my_instance) – or into a jsonv::value with jsonv::to_json(my_instance).

Deserializing with deserialize

Let's start with converting JSON into C++ types with jsonv::deserialize<T>.

#include <jsonv/parse.hpp>
#include <jsonv/value.hpp>
#include <iostream>
#include <string>
int main()
{
std::cout << "a=" << jsonv::deserialize<int>("1") << std::endl;
std::cout << "b=" << jsonv::deserialize<double>("2.5") << std::endl;
std::cout << "c=" << jsonv::deserialize<std::string>(R"("Hello!")") << std::endl;
jsonv::value val = jsonv::parse(R"({ "d": 4 })");
std::cout << "d=" << jsonv::deserialize<int>(val.at("d")) << std::endl;
}

Output:

a=1
b=2.5
c=Hello!
d=4

The first three deserialize from JSON text, which is the spelling to reach for when text is what you have: the C++ value is read straight out of the text, and no jsonv::value is built along the way. That does mean a C++ string handed to deserialize is JSON text rather than a JSON string, so deserialize<std::string>(R"("Hello!")") is Hello! while deserialize<std::string>("Hello!") fails to parse. The last deserializes from a jsonv::value, which is the spelling for JSON you have already parsed or built. Either way, JSON which does not hold what you asked for – deserialize<int>(R"("one")") – throws a jsonv::deserialization_error saying what was found instead.

Overall, this is not very complicated. We did not do anything that could not have been done through a little use of parse and the as_ accessors like as_integer. So what is this deserialize 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.

#include <iostream>
#include <string>
#include <string_view>
#include <utility>
class my_type
{
public:
{
if (!from.expect(jsonv::ast_node_type::object_begin))
throw jsonv::deserialization_error(context.problem_path(from), "Expected an object");
// Step off the { and onto the first key -- or onto the } of an empty object.
(void) from.next_token();
while (from.current_type() != jsonv::ast_node_type::object_end)
{
std::string key = from.current().visit_key([] (const auto& k) { return std::string(k.value()); });
// Step off the key and onto its value.
(void) from.next_token();
if (key == "a")
a = deserialize_member<int>(from, context, "a");
else if (key == "b")
b = deserialize_member<int>(from, context, "b");
else if (key == "c")
c = deserialize_member<std::string>(from, context, "c");
else
(void) from.next_value();
}
// Step off the } too, leaving the reader one past this object.
(void) from.next_token();
}
static const jsonv::deserializer* get_deserializer()
{
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:
template <typename T>
static T deserialize_member(jsonv::reader& from, jsonv::deserialization_context& context, std::string_view key)
{
auto mark = context.problems().size();
if (auto result = context.deserialize<T>(from))
return *std::move(result);
}
private:
int a = 0;
int b = 0;
std::string c;
};
int main()
{
jsonv::formats local_formats;
local_formats.register_deserializer(my_type::get_deserializer());
my_type x = jsonv::deserialize<my_type>(R"({ "a": 1, "b": 2, "c": "Hello!" })", format);
std::cout << x << std::endl;
}
auto visit_key(FVisitor &&visitor) const
Convenience function for calling std::visit on a key (see as_key).
Definition ast.hpp:498
An RAII guard naming one step of the deserialization path while it is alive.
Provides extra information to routines used for deserialization, collects the problems they encounter...
jsonv::path problem_path(const reader &from) const
Where to report a problem noticed while from is sitting on the thing that is wrong.
problem_list take_problems_since(problem_list::size_type mark)
Remove and return the problems recorded since mark, a value problems() previously reported the size o...
const problem_list & problems() const &
Get the problems encountered so far. If this list is empty, no problems have occurred.
std::expected< T, ast_node_type > deserialize(reader &from)
Attempt to deserialize a T from from using the formats associated with this context.
Exception thrown if there is any problem running deserialize.
A deserializer for a type T with a deserializing constructor.
A deserializer holds the method for converting JSON source into an arbitrary C++ type.
Simply put, this class is a collection of deserializer and serializer instances.
Definition formats.hpp:157
void register_deserializer(const deserializer *, duplicate_type_action action=duplicate_type_action::exception)
Register a deserializer that lives in some unmanaged space.
static formats defaults()
Get the default formats instance.
static formats compose(const list &bases)
Create a new (empty) formats using the bases as backing formats.
A reader instance reads from some form of JSON source (probably a string) and converts it into a JSON...
Definition reader.hpp:105
bool next_value() noexcept
Go to one past the value this reader is on.
ast_node_type current_type() const
Get the type of the current AST node, which is always current().type().
bool next_token() noexcept
Go to the next token.
std::expected< void, ast_node_type > expect(ast_node_type type) const
Check that the current AST node has the given type or is one of the expected types.
const ast_node & current() const
Get the current AST node this reader is pointing at.
Copyright (c) 2015-2026 by Travis Gockel.

Output:

{ a=1, b=2, c=Hello! }

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 a deserializing constructor. All that means is that it has those two arguments: a jsonv::reader and a jsonv::deserialization_context. The reader is a forward cursor over the JSON, and when the constructor is called it is sitting on the first token of the value to deserialize from – for my_type, the { of an object. From there, the constructor walks the object one key at a time, in whatever order the document wrote them:

while (from.current_type() != jsonv::ast_node_type::object_end)
{
std::string key = from.current().visit_key([] (const auto& k) { return std::string(k.value()); });
// Step off the key and onto its value.
(void) from.next_token();
if (key == "a")
a = deserialize_member<int>(from, context, "a");
// ...
else
(void) from.next_value();
}

Each value it wants is deserialized by deserialize_member, which leaves the reader on the next key, or on the }. A key it does not recognize has its value skipped with next_value, which steps over the whole value in one go, however large it is. Once the closing } has been stepped off as well, the reader is left one position past the object. Every deserializer promises that, because it is where whatever is deserializing around this object carries on from. A key the document leaves out leaves its member as it was initialized.

template <typename T>
static T deserialize_member(jsonv::reader& from, jsonv::deserialization_context& context, std::string_view key)
{
auto mark = context.problems().size();
if (auto result = context.deserialize<T>(from))
return *std::move(result);
}

The jsonv::deserialization_context is what does the work. context.deserialize<T>(from) deserializes a T from the value under the cursor, using the jsonv::formats the deserialization was started with, and leaves the cursor one past that value. When it cannot, it does not throw: it records the problem on the context and returns a std::unexpected. A constructor can only fail by throwing, so deserialize_member throws a jsonv::deserialization_error carrying what the context recorded – taking it with take_problems_since rather than copying it, so the problem is reported once. The path_scope names the member for as long as it is being deserialized, which is what puts a problem with "a" at .a – or at [3].a when the my_type is the fourth element of an array.

Giving up at the first problem is what deserialization does by default. With jsonv::deserialize_options::on_error::collect_all, it carries on past a problem so that it can report as many as it finds, and a deserializer which walks the reader itself has more to do for that to work – see jsonv::deserialization_context::recover. The DSL described below does all of that for you.

static const jsonv::deserializer* get_deserializer()
{
return &instance;
}

A jsonv::deserializer is a type that knows how to read JSON and create some C++ type out of it. In this case, we are creating a jsonv::deserializer_construction, which is a subtype that knows how to call the constructor of a type. There are all sorts of jsonv::deserializer implementations in jsonv/serialization/, so you should be able to find one that fits your needs.

jsonv::formats local_formats;
local_formats.register_deserializer(my_type::get_deserializer());

Now things are starting to get interesting. The jsonv::formats object is a collection of jsonv::deserializers, so we create one of our own and add the jsonv::deserializer* from the static function of my_type. The local_formats only knows how to deserialize instances of my_type – it does not know even the most basic things like how to deserialize 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!

my_type x = jsonv::deserialize<my_type>(R"({ "a": 1, "b": 2, "c": "Hello!" })", format);

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::deserialization_error complaining about how it did not know how to deserialize a my_type.

When the JSON came from a file, an error is more use if it says which file. Build the jsonv::deserialization_context yourself, with the name of the source last, and hand it to deserialize in place of the format:

std::nullopt,
nullptr,
"my_type.json"
);
my_type y = jsonv::deserialize<my_type>(R"({ "a": 1, "b": "two", "c": "Hello!" })", context);
Configuration for various deserialization options. This becomes part of the deserialization_context.
Represents an exact path in some JSON structure.
Definition path.hpp:107

The "b" is not an int, so this throws a jsonv::deserialization_error reading Deserialization error at my_type.json#.b: Read node of type string when expecting integer. Write out every argument before the name: the nullptr is the user data, and a string in its place would be taken for user data rather than for a name. A context is meant for one document, so make a new one for each file.

If you are coming from JSON Voorhees 1.x, you may be looking for extract_sub. A deserializing constructor used to be handed a whole jsonv::value and pull each member out of it by name, which is what extraction_context::extract_sub did. It is gone because that value is gone: deserialization reads the JSON as it goes rather than building a value first, and a forward cursor has no way to look a key up. Walking the keys, as my_type does, is what replaces it. When random access is genuinely wanted – what one member means depends on another written after it, say – read the object into a jsonv::value and look things up in that, with value::find or value::at_path. A deserializing constructor can still take a const jsonv::value& in place of the reader for exactly this, and is handed the object as a value – read off the reader with jsonv::read_value, if it was not one already. That keeps the extract_sub calls of a 1.x constructor easy to port:

my_type(const jsonv::value& from, jsonv::deserialization_context& context) :
a(deserialize_member<int>(from, context, "a")),
b(deserialize_member<int>(from, context, "b")),
c(deserialize_member<std::string>(from, context, "c"))
{ }
template <typename T>
static T deserialize_member(const jsonv::value& from,
const std::string& key
)
{
auto member = from.find(key);
if (member == from.end_object())
throw jsonv::deserialization_error(context.path(), "Missing required member");
return context.deserialize<T>(member->second);
}
jsonv::path path() const
Get the path currently being deserialized, as named by the live path_scope guards.

The path_scope is what says where a problem is, since a value has no idea where in the document it came from. That includes a member which is not there at all: looking it up with find rather than value::at is what reports a missing "b" at .b, while the scope is still alive to say so. The std::out_of_range from at would only be caught once the scope had gone, so it would be reported at the object around the member, and it does not say which member it was. context.deserialize<T> throws rather than returning when it is handed a value, so nothing needs handing over. Building that value for every object deserialized is the cost the reader-based constructor avoids.

Serializing with serialize and to_json

JSON Voorhees also converts from your C++ structures into JSON, using jsonv::serialize for JSON text and jsonv::to_json for a jsonv::value. It should feel like a mirror of jsonv::deserialize, with similar argument types and many shared concepts. Just like deserialization, both use the jsonv::formats class, but they look up a jsonv::serializer in it to convert from C++ into JSON. Where a deserializer reads from a jsonv::reader, a serializer writes into a jsonv::writer.

#include <jsonv/value.hpp>
#include <jsonv/writer.hpp>
#include <iostream>
#include <string>
#include <utility>
class my_type
{
public:
my_type(int a, int b, std::string c) :
a(a),
b(b),
c(std::move(c))
{ }
static const jsonv::serializer* get_serializer()
{
static auto instance = jsonv::make_serializer<my_type>
(
[] (const jsonv::serialization_context& context,
const my_type& self,
)
{
to.key("a");
context.serialize(self.a, to);
to.key("b");
context.serialize(self.b, to);
to.key("c");
context.serialize(self.c, to);
to.object_end();
}
);
return &instance;
}
private:
int a;
int b;
std::string c;
};
int main()
{
jsonv::formats local_formats;
local_formats.register_serializer(my_type::get_serializer());
my_type x(5, 6, "Hello");
std::cout << jsonv::serialize(x, format) << std::endl;
jsonv::value tree = jsonv::to_json(x, format);
std::cout << tree.at("c") << std::endl;
}
void register_serializer(const serializer *, duplicate_type_action action=duplicate_type_action::exception)
Register a serializer that lives in some managed space.
Provides extra information to routines used for serialization: the formats to find other serializers ...
Definition serialize.hpp:42
void serialize(const T &from, writer &to) const
Write from into to using the formats associated with this context.
Definition serialize.hpp:64
A serializer holds the method for converting an arbitrary C++ type into JSON, written token by token ...
writer & object_begin()
Write the token which opens or closes an object or an array.
writer & key(std::string_view key)
Write the key of the next member of the open object, including the separator.
writer & object_end()
Write the token which opens or closes an object or an array.
Copyright (c) 2015-2026 by Travis Gockel.
value to_json(const T &from, const formats &fmts)
Encode a JSON value from from using the provided fmts.

Output:

{"a":5,"b":6,"c":"Hello"}
"Hello"

The serializer is handed a jsonv::writer positioned where the my_type goes – at the root here, but just as well as the element of an array or the value of another object's member – and writes exactly one value there. It opens an object, and for each member writes the key and then hands the member to context.serialize, which looks up the serializer for the member's type in the same jsonv::formats and has it write the member where the writer now is. The writer writes the punctuation, and refuses a token which does not belong where it is: a key outside an object, say, or a value inside one with no key before it.

jsonv::serialize writes x as compact JSON text with nothing built in between: no jsonv::value is made along the way, and the members come out in the order the serializer wrote them. jsonv::to_json runs the same serializer into a jsonv::value_encoder and hands back the tree it built, for when a jsonv::value is what you want – to look things up in, as here, or to change before writing it out. A jsonv::value keeps an object's members sorted by key, so the text of one can list them in a different order from serialize; for my_type, they happen to agree. To write the text to a stream rather than into a std::string, there is jsonv::serialize(x, std::cout, format); and jsonv::serialize(x, to, format) writes x into a jsonv::writer of your own, as in Encoding and decoding.

A serializer written for JSON Voorhees 1.x returned a jsonv::value rather than writing one. That still works: jsonv::make_serializer also takes a function of (const jsonv::serialization_context&, const T&), or of just the const T&, which returns a jsonv::value to be written whole. Building that value for every object serialized is the cost the writer avoids.

Composing Type Adapters

Does all this seem a little bit manual to you? Creating a deserializer 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:

jsonv::formats formats =
.type<foo>()
.member("a", &foo::a)
.member("b", &foo::b)
.default_value(10)
.member("c", &foo::c)
.type<bar>()
.member("x", &bar::x)
.member("y", &bar::y)
.member("z", &bar::z)
.since(jsonv::version(2, 0))
.member("w", &bar::w)
.until(jsonv::version(5, 0))
.compose_checked(jsonv::formats::defaults())
;
Represents a version used to deserialize and encode JSON objects from C++ classes.
Definition version.hpp:24

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 deserialize 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 couple of the many functions available to modify how the members of the type get transformed.

The chain ends with compose_checked, which checks that every type the members refer to – int and std::string here – can be deserialized and serialized once the adapters the DSL built are combined with jsonv::formats::defaults, and then composes the two, just as we composed local_formats by hand earlier.

The formats we built would be perfectly capable of serializing to and deserializing 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"
}

Deserializing a bar reads the document just the way the constructor of my_type did: each object's keys are walked in the order the document wrote them, each is handed to the member which claims it, and a key no member claims has its value stepped over unread. What the DSL adds is everything that constructor left out. A member without a default_value is required, a key the document repeats is settled by jsonv::deserialize_options::on_duplicate_key, and jsonv::deserialize_options::on_error::collect_all carries on past a problem to report the rest.

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 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.

#include <jsonv/value.hpp>
#include <iostream>
int main()
{
jsonv::value x = 5;
std::cout << jsonv::map([] (const jsonv::value& y) { return y.as_integer() * 2; }, x) << std::endl;
}
A collection of algorithms a la &lt;algorithm&gt;.
int64_t as_integer() const
Get this value as an integer.
JSONV_PUBLIC value map(const std::function< value(const value &)> &func, const value &input)
Run a function over the values in the input.

If everything went right, you should see a number:

10

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...

#include <jsonv/value.hpp>
#include <iostream>
int main()
{
std::cout << jsonv::array({ 1, 2, 3, 4, 5 })
.map([] (const jsonv::value& y) { return y.as_integer() * 2; })
<< std::endl;
}

Now we're starting to get somewhere!

[2,4,6,8,10]

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.

#include <jsonv/parse.hpp>
#include <jsonv/value.hpp>
#include <iostream>
int main()
{
jsonv::traverse(jsonv::parse(std::cin),
[] (const jsonv::path& path, const jsonv::value& value)
{
std::cout << path << " => " << value << std::endl;
},
true
);
}
JSONV_PUBLIC void traverse(const value &tree, const std::function< void(const path &, const value &)> &func, const path &base_path, bool leafs_only=false)
Recursively walk the provided tree and call func for each item in the tree.

Now we have a tiny little program to decompose JSON into jq style path expressions and their values. For example, if you pipe { "bar": [1, 2, 3], "foo": "hello" } into the program:

.bar[0] => 1
.bar[1] => 2
.bar[2] => 3
.foo => "hello"

All of the really powerful functions can be found in algorithm.hpp. My personal favorite is jsonv::merge. The idea is simple: it merges two (or more) JSON values into one.

#include <jsonv/value.hpp>
#include <iostream>
int main()
{
jsonv::value a = jsonv::object({ { "a", "taco" }, { "b", "cat" } });
jsonv::value b = jsonv::object({ { "c", "burrito" }, { "d", "dog" } });
jsonv::value merged = jsonv::merge(std::move(a), std::move(b));
std::cout << merged << std::endl;
}
value merge(TValue &&... values)
Merges all the provided values into a single value.

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
https://github.com/tgockel/json-voorhees
http://json.org/