Simple modélisation de JSON en C++
Avec Boost.Variant
Il existe un certain nombre de parsers et autres librairies en C++ pour traiter du JSON (voir en bas de la page). Il manquerait Boost.PropertyTree qui a récemment été intégré dans boost.
En ce qui me concerne, je n'ai que très rarement besoins de manipuler du JSON de fond en comble, c'est à dire, par exemple, parser une chaine en JSON ou écrire dans un fichier avec ce format. Ce qui m'est utile en revanche, est de balancer des valeurs JSON-esques dans un container qui servira ensuite d'input à d'autres composants. C'est vague, je sais, c'est fait pour.
Le container en question est donc sommé d'accepter des strings, tableaux, objets, booléens etc. Bref, tout ce qui fait l'intérêt de JSON. Une des techniques que j'utilise est basée sur Boost.Variant. L'intérêt de Boost.Variant par rapport à Boost.Any est qu'il est type safe. Un mécanisme de visiteur permet de manipuler précisément les types wrappés, ce qui est utile lorsque le container en question n'est qu'un container de "passage" entre composants. L'autre intérêt de variant est sa propension à être récursif, tout comme JSON. Ca tombe bien.
Dans le code qui suit, json_type est le type généric qui modélise une valeur JSON. json_array, json_object sont des json_type. En plus, on accepte des std::string, bool etc. Ce n'est pas une implémentation de JSON fidèle au bit près. Comme je le disais, je veux juste du JSON-esque:
typedef boost::make_recursive_variant
<
int
, double
, bool
, std::string
, std::unordered_map<std::string, boost::recursive_variant_> // object
, std::vector<boost::recursive_variant_> // array
>::type json_type; // notre type generic
typedef std::unordered_map<std::string, json_type> json_object;
typedef std::vector<json_type> json_array;
On notera l'utilisation de std::unordered_map. Un boost::unordered_map ferait tout autant l'affaire. boost::make_recursive_variant
permet de rendre le variant récursif. Il accepte dès lors sans broncher un std::vector<boost::recursive_variant_>. Vous aurez compris qu'il s'agit de la modélisation du array JSON. Un typedef qui va bien permet de déclarer json_array à partir de std::vector<json_type>. La boucle est bouclée.
A l'utilisation, ça donne ce genre de chose:
json_array arr {3.14, 21, true}; //initializer list goodies (C++0x)
json_object obj;
obj["key"] = 42;
obj["str"] = std::string("hello");
obj["arr"] = arr;
Ok, mais faire ça ne sert à rien. Effectivement. Rappelez-vous, il s'agit d'un container de passage. Un autre composant va devoir se démerder (encore une fois, sans broncher) pour faire quelque chose d'utile avec. C'est là que boost::static_visitor entre en jeu. On va pouvoir traiter finement les types wrappés (std::string, int, std::vector...). Le principe est le suivant:
// un type T quelconque (pseudo code)
struct json_visitor : public boost::static_visitor<T>
{
T operator()(const json_object& val) const
{
//...
}
T operator()(const json_array& val) const
{
//...
}
T operator()(const std::string& val) const
{
//...
}
// ...
};
// on applique le visitor à notre json_type
boost::apply_visitor(json_visitor(), json_type);
Par exemple, faire un output au format JSON de notre json_type. On va déclarer un visitor dérivant de boost::static_visitor<std::string> qui se chargera de formater le tout:
struct json_conv_visitor : public boost::static_visitor<std::string>
{
std::string operator()(const json_object& val) const
{
std::ostringstream os;
os << "{";
for (json_object::const_iterator i(val.begin()), e(val.end())
; i != e; ++i)
{
os << i->first << ": "
<< boost::apply_visitor(json_conv_visitor(), i->second)
<< ", ";
}
os << "}";
return os.str();
}
std::string operator()(const json_array& val) const
{
std::ostringstream os;
os << "[";
for (json_array::const_iterator i(val.begin()), e(val.end())
; i != e; ++i)
{
os << boost::apply_visitor(json_conv_visitor(), *i)
<< ", ";
}
os << "]";
return os.str();
}
std::string operator()(const std::string& val) const
{
return std::string("\"").append(val).append("\"");
}
template<typename T> // le reste est refilé à boost::lexical_cast
std::string operator()(const T& val) const
{
return boost::lexical_cast<std::string>(val);
}
};
Appliqué à l'exemple plus haut, ça retourne:
{str: "hello", key: 42, arr: [3.1400000000000001, 21, 1, ], }
Voila, le code complet est dispo sur github ici. Si ça vous intéresse, servez-vous. Un de ces 4, on parlera d'une manière de parser une chaine JSON pour filler ce genre de variant, sans utiliser Spirit. Sur ce, a+