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.
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.
Other json parsers are curretly not supported by our utility functions.
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).
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.