Is there a way to specify simpler JSON (de-) serialization for std :: map using Cereal / C ++?

The project I'm working on is a C ++ application that manages a large number of custom hardware devices. The application has a socket / port interface for the client (e.g. a graphical interface). Each type of device has its own well-defined JSON scheme, and we can serialize those who have Cereal just fine.

But the application should also parse incoming JSON requests from the client. One part of the query indicates device filter options, similar to the SQL 'WHERE' clause, in which all AND expressions are combined. For example:.

"filter": { "type": "sensor", "status": "critical" } 

This means that the client wants to perform an operation on each β€œtouch” device with a β€œcritical” status. At first glance, it seemed that the C ++ implementation for the filter parameters would be std :: map. But when we experimented with using Cereal to deserialize an object, it failed. And when we serialize the hardcoding card, it looks like this:

 "filter": [ { "key": "type", "value": "sensor" }, { "key": "status", "value": "critical" } ] 

Now I can understand why Cereal supports such verbose serialization of the map. In the end, the card key may be non-lowercase. But in this case, the key is a string.

I do not really like to rewrite our interface specification and force our clients to generate explicitly non-idiomatic JSON to satisfy Cereal. I am new to Cereal and we are stuck on this issue. Is there any way to tell Cereal to parse this filter as std :: map? Or maybe I'm asking for it wrong. Is there another stl container we need to deserialize?

+7
c ++ json serialization stl cereal
Mar 21 '14 at 21:26
source share
1 answer

Let me first tell you why cereal gives out a more detailed style than the one you may wish for. The kernel is written to work with arbitrary serialization archives and uses an average approach that satisfies all of them. Imagine that a key type is something more complex than a string or arithmetic type - how can we serialize it in the simple way "key" : "value" ?

Also note that the grain expects to be the prototype of any data that it reads.




Considering what you want, it is possible with cereals, but there are several obstacles:

The biggest hurdle to overcome is the fact that the desired input serializes some unknown number of name / value pairs inside the JSON object, rather than a JSON array. the grain was designed to use JSON arrays when working with containers that can contain a variable number of elements, as this made the most sense when using the basic quickjson analyzer that it uses.

Secondly, cereals do not currently expect that a name in a pair of name-values ​​will actually be loaded into memory - it simply uses them as an organizational tool.




So, hassle-free execution, here is a fully working solution (can be made more elegant) for your problem with minimal changes in cereals (this actually uses the change for cereals 1.1 , current version 1.0):

Add this function to the JSONInputArchive :

 //! Retrieves the current node name /*! @return nullptr if no name exists */ const char * getNodeName() const { return itsIteratorStack.back().name(); } 

Then you can write a serialization specialization for std::map (or unordered, depending on what you prefer) for a couple of lines. Be sure to put this in the cereal namespace so that it can be found by the compiler. This code should exist in your own files somewhere:

 namespace cereal { //! Saving for std::map<std::string, std::string> template <class Archive, class C, class A> inline void save( Archive & ar, std::map<std::string, std::string, C, A> const & map ) { for( const auto & i : map ) ar( cereal::make_nvp( i.first, i.second ) ); } //! Loading for std::map<std::string, std::string> template <class Archive, class C, class A> inline void load( Archive & ar, std::map<std::string, std::string, C, A> & map ) { map.clear(); auto hint = map.begin(); while( true ) { const auto namePtr = ar.getNodeName(); if( !namePtr ) break; std::string key = namePtr; std::string value; ar( value ); hint = map.emplace_hint( hint, std::move( key ), std::move( value ) ); } } } // namespace cereal 



This is not the most elegant solution, but it works well. I left everything in the general template, but what I wrote above will only work in JSON archives, given the changes made. Adding a similar getNodeName() to the XML archive will most likely allow it to work there too, but obviously this doesn't make sense for binary archives.

To make this clean, you want to place enable_if around this for the archives it works with. You will also need to modify JSON archives in cereals to work with variable-sized JSON objects. To get an idea of ​​how to do this, see how the kernel sets the state in the archive when it receives the SizeTag series. Basically you need to make the archive not open the array and open the object instead, and then create your own version of loadSize() , which will see how large the object is (this will be Member in quickjson).




To see this in action, run this code:

 int main() { std::stringstream ss; { cereal::JSONOutputArchive ar(ss); std::map<std::string, std::string> filter = {{"type", "sensor"}, {"status", "critical"}}; ar( CEREAL_NVP(filter) ); } std::cout << ss.str() << std::endl; { cereal::JSONInputArchive ar(ss); cereal::JSONOutputArchive ar2(std::cout); std::map<std::string, std::string> filter; ar( CEREAL_NVP(filter) ); ar2( CEREAL_NVP(filter) ); } std::cout << std::endl; return 0; } 

and you will receive:

 { "filter": { "status": "critical", "type": "sensor" } } { "filter": { "status": "critical", "type": "sensor" } } 
+5
Mar 23 '14 at 5:40
source share



All Articles