In several of my crates I found myself writing custom serializers/deserializers for byte-slice-like objects (something that can be easily represented as a byte slice and restored from one; e.g. byte slices themselves, GenericArray<u8> and so on) which would serialize to bytes if is_human_readable() is false, and to base64/hex otherwise. So, for example, in some custom struct I can say
At this point it seems to be worth extracting to a separate crate, but I wonder if such a crate already exists, or perhaps serde itself has this functionality somehow built-in.
I am maintaining the serde_with crate where I collect various of such helper functions. There are many such helper crate, since serde will not carry them.
In serde_with there is nothing yet which makes use of the is_human_readable flag. A single function like serialize_bytes_as_hex feels too restrictive for a general crate. The reason it is too restrictive, is that this is only one possible combination, but as you already wrote, base64, with all its variants, might also be interesting here, or maybe using Display which I have seen for IP addresses.
A more general version could look like IfHumanReadable<Hex, Bytes>, which would require that the field can be serialized using either Hex or Bytes and picks based on the is_human_readable flag. This design allows you to plug in the correct variants for both possibilities. If a shorter name is desired, it is always possible to add a type alias.
Thanks, I'll see if I can perhaps come up with some API that would fit serde_with. I am not sure I understand your IfHumanReadable proposition though - is it supposed to be a trait? And Hex and Bytes are types implementing From<T> (where T is our type) and Serializable? There may be problems with defining generic traits over foreign types.
In serde_with I try to explore how you can compose custom serialization logic from smaller parts. It works with two traits SerializeAs/DeserializeAs, which are mostly like the serde equivalent ones, expect that they allow you to extend it with new types and to customize containers (think Vec, HashMap). The IfHumanReadable would be a zero sized struct with two generic parameters. It would implement
impl<T, IF, ELSE> SerializeAs<T> for IfHumanReadable<IF, ELSE>
where
IF: SerializeAs<T>,
ELSE: SerializeAs<T>,
{...}
IfHumanReadable would behave identical to IF, if the format is human readable, and identical to ELSE if not. Hex and Bytes are two other zero sized types implementing SerializeAs. I was mainly referring with their names to your original post, but two such types already exists in the crate.
I hope this clarifies the situation a bit.
This "trick" of having a zero sized type with generic arguments is a nice way of passing (static) arguments to the serialization code, which also works with plain serde.