USCiLab / cereal

A C++11 library for serialization
BSD 3-Clause "New" or "Revised" License
4.2k stars 754 forks source link

Begin serialization without enclosing parent node? #303

Open rcdailey opened 8 years ago

rcdailey commented 8 years ago

I want to write my object to json or xml archive without the preliminary wrapper node around the root-level data. This situation is covered perfectly in this stack overflow question:

http://stackoverflow.com/questions/33726072/how-to-serialize-a-json-object-without-enclosing-it-in-a-sub-object-using-cereal

My sample code:

MyClass myclass;

std::ostringstream ss;
cereal::XMLOutputArchive archive(ss);
archive(myclass);

The above yields the wrapping object which I do not want (named "value0"). Per the SO answer linked previously, the solution is:

myclass.serialize(archive);

However, this will not work if there are separate save/load functions or other types of callbacks.

Is there a built-in mechanism to disable the root node on develop?

rcdailey commented 8 years ago

Also this solution does not work if myclass is const (in conjunction with output archives, which is normally valid).

AzothAmmo commented 8 years ago

There's no built in mechanism for this but you could modify xml.hpp or json.hpp to achieve what you want. In the constructors for the output archives we start the root node, which you could selectively disable by adding some parameters to the options struct.

You may have problems loading because you won't be loading valid XML/JSON documents. You could likely modify the input archives to handle this as well.

rcdailey commented 8 years ago

I think you are missing the idea here. I'm not suggesting we remove the root XML node. But rather, the 2nd child that is created. Example:

<value0>
  <value0>
    <value0>foo</value0>
    <value1>bar</value1>
  </value0>
</value0>

The 2nd child is value0 and unnecessary. It is only added because I do archive(myclass). Instead, I want the XML output like:

<value0>
  <value0>foo</value0>
  <value1>bar</value1>
</value0>

This isn't invalid XML.

rcdailey commented 8 years ago

Any thoughts @AzothAmmo ?

AzothAmmo commented 8 years ago

I haven't had time to look into this much yet, but I do understand what you want to do. Without thinking through all of the implications, I think this could potentially be implemented via a wrapper class that you would use something like:

ar( cereal::make_minimal( myData ) );

Which would cause myData to be serialized without an explicit node for itself as in your example.

I think this is a lot cleaner (implementation-wise) than adding new serialization function variants.

rcdailey commented 8 years ago

I guess my point is, the current behavior seems undesirable. Is there a reason to have another root under the root you already add at the archive level? Isn't it redundant? I can't think of a single use case for it.

It seems like the latter example output I provided in an earlier reply should be the default behavior.

Unless the use case is to support minimal types passed directly to the archive? I can understand then, but doesn't seem like a practical scenario.

I like your idea of flexibility I am just questioning if the current behavior has valid use cases.

AzothAmmo commented 8 years ago

It is the way it is because we need to support serializing multiple objects to the same archive.

rcdailey commented 8 years ago

That's true! Good point. I'll look forward to your suggested solution.

On Mon, Jul 11, 2016, 6:45 PM Shane Grant notifications@github.com wrote:

It is the way it is because we need to support serializing multiple objects to the same archive.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/USCiLab/cereal/issues/303#issuecomment-231898563, or mute the thread https://github.com/notifications/unsubscribe/ABr6doe0DgCStNCOa4DTAUfcdT1H6WxMks5qUtWtgaJpZM4I7E-l .

rcdailey commented 8 years ago

So I am working on a make_minimal implementation, however I'm not sure how I should do it. I have a rough idea I'm trying to make work:

namespace cereal
{
   namespace detail
   {
      template<typename T>
      class minimal_wrapper
      {
      public:
         minimal_wrapper(T& obj)
            : m_obj(obj)
         {}

         template<typename Archive>
         std::string save_minimal(Archive const& ar)
         {
            // save here somehow
         }

         template<typename Archive>
         void load_minimal(Archive const& ar, std::string const& value)
         {
            // load here somehow
         }

      private:
         T& m_obj;
      };
   }

   template<typename T>
   detail::minimal_wrapper<T> make_minimal(T& obj)
   {
      return detail::minimal_wrapper<T>{obj};
   }
}

However, I'm not sure how I should implement save_minimal and load_minimal in the wrapper. Because the archive is const, I guess that means I can't use it (not even sure why it's passed in to begin with).

Do you have any ideas? I want to implement this externally first and see how it works, then I'm happy to do a PR.

AzothAmmo commented 8 years ago

My idea was more along the lines of how NameValuePair and the traits for xxx_minimal work. Essentially the class would be a very thin wrapper that is detected by prologue/epilogue functions and sets up state appropriately.

Naming it make_minimal was not a good choice on my part. You can't directly use save or load minimal because these expect a single value to be serialized.

I'm a bit worried that there may be ways to break this mechanism through some combination of nesting it and using out of order loading.

rcdailey commented 8 years ago

What this is effectively enabling is a "passthrough" to children, or a "make children into siblings" functionality.

In that case, wouldn't it be reasonable to make it function as if the intermediate class doesn't exist at all? The children will be treated as siblings to the parent that initiated its serialize function.

But honestly this isn't something we need to overgeneralize. I can't even make a good argument for using this functionality outside of the root level invocation to the archive.

Can't we just make it a simple on/off flag at the archive level?

MyObj myobj;
std::istringstream ss{"some json data"};
cereal::JSONInputArchive archive(ss, no_siblings); // 'no_siblings' could be an enumerator provided by cereal; one of many to add constraints to or change behavior of archives
archive(myobj);

Effectively this forces the serializable class (MyObj in this case) to write the root-level node. It is also an error to use an archive with this option set for more than 1 sibling object (you can detect this and throw for num args > 1.

Much simpler, less scenarios to consider. Similar to this, maybe a CRTP derived class that can be used to change this behavior:

cereal::SingleUseArchive<cereal::JSONInputArchive> archive(ss);
archive(myobj);
archive(mysecondobj); // throws

Food for thought...

rcdailey commented 8 years ago

@AzothAmmo Would it be possible for you to help me come up with a solution to your make_minimal idea? I'm happy to help contribute a change, I just need some help getting started.

This is a huge problem right now for me so I need a solution.

spavlenko commented 6 years ago

If you are using external serialize function, u also can fix that problem calling this function directly:

template <class  Archive>
    void serialize(Archive & ar, ReporRegistrationData& d)
    {
        ar(cereal::make_nvp("app_id", d.app_id),
            cereal::make_nvp("app_version", d.app_version),
            cereal::make_nvp("hw_id", d.hw_id),
            cereal::make_nvp("event_type", d.event_type),
        );
    }

And then for serialization:

    std::stringstream stream(std::ios_base::out | std::ios_base::binary);
    {
        cereal::JSONOutputArchive serializer(stream);
        cereal::serialize<cereal::JSONOutputArchive>(serializer, data);
    }

    const auto res = stream.str();

for deserialization (because cereal also doesn't wanna deserialize without enclosing parent node ):

     std::stringstream stream(std::ios_base::in | std::ios_base::out);
     stream.write(json.data(), json.size());

     cereal::JSONInputArchive deserializer(stream);
     cereal::serialize<cereal::JSONInputArchive>(deserializer, data);

Hope it will help somebody.

LowCostCustoms commented 6 years ago

Hi there. I wrote simple header that help to perform inline serialization (without versioning): https://github.com/LowCostCustoms/cereal-inline/blob/master/cereal_inline.hpp

Devacor commented 5 years ago

@LowCostCustoms is a versioning supported version hard to make? I took a look but immediately ran into a bit of trouble so thought I'd ask if anyone else has tried?

wrightleft commented 4 years ago

I'm running into this with serializing a std::vector. I get:

{
  "value0": [
    1,
    2,
    3
  ]
}

But I want:

[
  1,
  2,
  3
]

This was my first test with cereal, and I can't figure out how to get what I want. It might be back to rapidjson for me. :(

uentity commented 4 years ago

@wrightleft Give you vector a meaningful name via make_nvp