Description
Problem
Hello! I'd like to address an issue that has been raised numerous times in this repository. Namely, I'd like to propose context-dependent serialization based on certain attributes on fields in a struct.
Consider this simple example:
/// RcLogin packet.
#[derive(Debug, Serialize)]
pub struct RcLogin {
/// Encryption key.
pub encryption_key: u8,
/// Version.
pub version: String,
/// Account.
// TODO: This needs to be handled slightly differently by our custom serializer
pub account: String,
/// Password.
// TODO: This needs to be handled slightly differently by our custom serializer
pub password: String,
/// PC IDs
pub identification: Vec<String>,
}
The two fields above, account
and password
, are Strings encoded in a special format in the format I am writing the serializer for. More specifically, the custom serializer should encoded the length information of the string, but only for these two types. Everything else is "normal" serialization (i.e. just encoding the string with no additional information).
Currently, the way you can do this is by implementing serialize_with
, deserialize_with
, and with
. The issue with this approach with respect to custom serializers is that as far as I'm aware, it's impossible for these functions to access the internal state of the custom serializer / deserializer. See: https://github.com/Preagonal/preagonal-client-rs/blob/253164d83aa52641d2dcaf066c0c7a1f650ab2b1/src/net/serialization/serialize.rs#L13-L15
The way I solved the above issue, and still getting the internal state of the serializer, is actually using a newtype (called GString
) with some unsafe code, which is definitely not my preferred solution:
fn serialize_newtype_struct<T>(
self,
name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
match name {
"GString" => {
// TODO(@ropguy): The following unsafe cast is used to extract a GString.
let gstring: &GString = unsafe { &*(value as *const T as *const GString) };
self.writer.write_gstring(&gstring.0)?;
Ok(())
}
_ => todo!(),
}
}
And you might be wondering: why not impl Serialize for GString
? As mentioned, I'm looking for a way to customize the serialization logic for only my serializer. If there was another serializer, like JSON, that wanted to serialize my type, it shouldn't be locked in to the GString
serialization.
Previous Issues
There are a few other issues that have been raised in this repository with this as a feature request. Please see this comment on this closed issue: #2309 (comment)
In addition, this is a current open issue in Serde that has a similar request: #2877
Supporting XML-like documents is not a goal for serde. You would be better off using an XML-specific derive macro.
I understand not wanting to support XML-like documents, but something that is a global serialization / deserialization strategy should not be so opinionated when it comes to a quality-of-life feature such as custom attributes, especially when it doesn't just relate to XML documents.
Proposal
Like #2877, I'd like to propose a broader solution. Instead of adding namespace support, serde should allow custom attributes to be specified like so:
/// RcLogin packet.
#[derive(Debug, Serialize)]
pub struct RcLogin {
/// Encryption key.
pub encryption_key: u8,
/// Version.
pub version: String,
/// Account.
#[serde(attr = "gstring")]
pub account: String,
/// Password.
#[serde(attr = "gstring")]
pub password: String,
/// PC IDs
pub identification: Vec<String>,
}
Then, the custom serializer / deserializer will somehow be able to access the attr
and perform more context-dependent serialization. In my case, I would forego
Other Libraries
I think the way Kotlin does things with its @SerialInfo
annotation is a clean way to solve this: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serial-info/
Final Thoughts
Assuming the above aligns with the maintainer's design philosophy, I'm happy to file a PR.