JSON utilities#
We typically use and recommend json as a data format for input parameters to our codes. Json is easy for both humans and machines to read and write.
In order to parse json files in C++ we
recommend the jsoncpp
or nlohman/json
library.
Both are widely used, very well maintained and even available as a linux package libjsoncpp
and nlohmann-json3-dev
.
The advantage of nlohmann-json is that it is header only, while jsoncpp needs to be precompiled and linked.
How to start#
Json parsers work quite well already out of the box. For some repetitive tasks specific to scientific simulations we have written utiltiy functions. These can be accessed by influding
#include "dg/file/json_utilities.h"
By default this header automatically includes jsoncpp’s "json/json.h"
and thus incurs a dependency on jsoncpp. If one prefers to use nlohmann-json
one can define the macro
#define DG_USE_JSONHPP
#include "dg/file/json_utilities.h"
Now, the file <nlohmann/json.hpp>
is included instead of jsoncpp’s header.
Note
Other json parsers are curretly not supported by our utility functions.
File opening#
Let us assume that in python we write an input file
import json
params = {
"grid" : {
"n" : 3,
"Nx" : 64,
"x" : [0,1],
},
"bc" : ["DIR", "PER"],
}
with open("test.json") as f:
json.dump( params, f, indent = 4)
Now, in C++ we open this file conveniently using
#include <iostream>
#include "dg/file/json_utilities.h"
int main()
{
dg::file::JsonType js; // either Json::Value or nlohmann::json
try{
js = dg::file::file2Json( "test.json");
}catch( std::exception& e)
{
std::cout << e.what();
}
return 0;
}
In this example we open a file where C-style comments are allowed but discarded, while an error on opening or reading the file leads to a throw (which if we hadn’t captured leads to immediate abortion of the program).
The WrappedJsonValue class#
For our purposes the only downside of jsoncpp or nlohmann-json is that missing values do not trigger a throw and to manually check existence somewhat clutters the code. At the same time missing values often result from silly mistakes, which in a high performance computing environment result in real avoidable cost (for example because you ran a simulation with a default value that you did not really intend to).
For this reason we provide the dg::file::WrappedJsonValue
class.
It basically wraps the
access to a Json::Value
with guards that raise exceptions or display warnings in case an error occurs, for example when a key is misspelled,
missing or has the wrong type.
The goal is the composition of a good error message that helps a user
quickly debug the input file.
The Wrapper is necessary because json parsers by default silently
generate a new key in case it is not present which in our scenario is an
invitation for stupid mistakes.
You can use the WrappedJsonValue
like a Json::Value
with read-only access:
#include <iostream>
#include "dg/file/json_utilities.h"
int main()
{
dg::file::WrappedJsonValue ws; // hide Json type ...
try{
ws = dg::file::file2Json( "test.json");
}catch( std::exception& e)
{
std::cout << e.what();
}
try{
// Access a nested unsigned value
unsigned n = ws["grid"]["n"].asUInt();
// Access a nested unsigned value
unsigned Nx = ws["grid"]["Nx"].asUInt();
// Access a list item
double x0 = ws["grid"]["x"][0].asDouble();
// Acces using default initializer
double x1 = ws["grid"]["x"].get( 1, 42.0).asDouble();
// Access a string
std::string bc = ws["bc"][0].asString();
// Example of a throw
std::string hello = ws[ "does not exist"].asString();
} catch ( std::exception& e){
std::cout << "Error in file test.json\n";
std::cout << e.what()<<std::endl;
}
// Error in file test.json
// *** Key error: "does not exist": not found.
return 0;
}
A feature of the class is that it keeps track of how a value is called. For example
void some_function( dg::file::WrappedJsonValue ws)
{
int value = ws[ "some_non_existent_key"].asUInt();
std::cout << value<<"\n";
}
try{
some_function( ws["grid"]);
} catch ( std::exception& e){ std::cout << e.what()<<std::endl; }
// *** Key error: "grid": "some_non_existent_key": not found.
The what string knows that "some_non_existent_key"
is expected to be
contained in the "grid"
key, which simplifies debugging to a great extent.