serde icon indicating copy to clipboard operation
serde copied to clipboard

Add a `SerializeOwned` trait for owning serialization

Open cactusdualcore opened this issue 1 month ago • 4 comments

TL;DR: Serialize::serialize takes values by reference but a lot of the time they are thrown away after serialization

I really like Rust! Especially its destructive move semantics. However, serde force a pattern for serialization which reminds me a lot of C++ move semantics. Many times have I built up a value just to serialize it to some stream, dropping it right afterwards. However, because the Serialize::serialize method takes the value by reference, any transformations to convert a value from its in-memory representation to one more suitable for storage force users into either cloning excessively or writing a lot of Boilerplate to avoid it.

I think many use cases could profit from destructive serialization.

My suggestion is a new SerializeOwned trait, such that &T: SerializeOwned where T: Serialize.

Now, the Serializer trait could add a new method serialize_owned, with a default implementation that simply panics on use (ala unimplemented!()). Implementations could then move over to the new trait and if serde ever has a major version bump, just remove the default impl and instead provide it for serialize.

This would make writing custom serialization a lot less of a pain in these cases.

cactusdualcore avatar Nov 16 '25 16:11 cactusdualcore

I hope this sounds somewhat understandable 😅

cactusdualcore avatar Nov 16 '25 16:11 cactusdualcore

Honestly, it's not very clear why passing a reference makes you clone. Not vice versa? An example would be helpful.

Mingun avatar Nov 16 '25 20:11 Mingun

I have a program which starts by loading an object from a file, does some operations on it, then writes it back to the file.

Its data model looks roughly like some:

#[derive(Serialize)]
struct Foo { name: String }

struct Group { foos_by_key: Vec<Key> }

struct Bar {
  foos: Map<Key, Foo>,
  groups: Vec<Group>
}

I like human-readable files and so I decided that I wanted to refer to the Foos by name in the serialization of Bar::groups, like this:

{
  "foos": [
    { "name": "alice"  },
    { "name": "bob" }
  ],
  "groups": [
    {
       "foos_by_name": [ "alice", "bob" ]
    }
  ]
}

This means that while serializing groups I need to look up keys in foos, thus requiring a custom Deserialize impl for some wrapper over a reference to foos and a Group. But now I can't just serialize Bar::groups, because Group is not serializable, only its wrapper is!

There are two approaches:

  1. iterate over the the Vec<Group>, transform it to the desired, then collect it into another Vec. This is effectively an unnecessary clone of the Vec.
  2. use an Iterator for serialization.

I went with the second option, because it can be made generic over the iterator, which helps, because this exact problem happens already twice in the above example.

However, a &mut is required to drive an Iterator, which makes sense, but does not easily work with serialization. In theory, this can be sidestepped by interior mutability, but I wasn't sure whether this is even fine. It should be, but it somewhat breaks the contract of Serialize, because it breaks the contract that serialization is idempotent. Instead I made a wrapper around an Iterator, which clones it beforehand, so that it may be used. For the specific case of Iterators, this kinda already exists as the Serializer::collect_* methods, but it still requires a wrapper type which clones the iterator.

This might be a contrived example, but assume I wanted to serialize a File by contents. I could first std::fs::read_to_string, then serialize that and finally drop the string buffer used to hold the contents, but it feels icky to me to immediately drop a heap allocated buffer just to pass a reference.

cactusdualcore avatar Nov 19 '25 09:11 cactusdualcore

It might also allow serialization of std::sync::Exclusive and similar types (e.g. bevy's SyncCell).

cactusdualcore avatar Nov 19 '25 09:11 cactusdualcore