Posted on:
In this post I'd like to introduce a serdebug
helper which is a drop-in replacement for #[derive(Debug)]
with some of the advanced features that serde
can provide.
Yes, it's meant to be read as "ser"+"debug" (serialise to Debug) or "serde"+"debug" (Debug with serde) and not the way you thought ;).
Why would I want it? #
Often enough, in Rust I've ran into situations where I wanted to make Debug
representations more readable by hiding certain fields (like PhantomData
or similar markers that don't contain any useful information) or providing custom representation for some of them (which is especially hard in case of third-party types).
Unfortunately, what this usually means is a choice between giving up on auto-derived representation completely and implementing Debug
for your types manually, which might be quite verbose for large structures or enums, or wrapping types into intermediate newtype, which is a popular pattern in Rust lands, but not very convenient one (IMO), as it pollutes every usage side and/or requires reimplementing all traits you care about on top of the wrapper (often manually, unless you are lucky and it's easy to #[derive(...)]
with existing crates).
When I looked for solutions on crates.io, surely, I found that some of these issues were already tackled by others - for example, you can use debug_stub_derive
to provide custom formatting function for certain fields or derivative
to also unwrap newtypes or rename fields (the latter also has a number of customisations for other standard traits like Hash
, Clone
, PartialEq
, etc., so definitely check it out if you ever wanted to customise them too).
What bothered me however, is that they still felt not flexible enough, and at the same time I already use serde
for most of my types which provides these and many more customisations out of the box, as it has to deal with all sorts of output formats, and usually I'd just want my debug representation to use the same names and fields as I'm already serialising, expect in a more human-readable format that Debug
provides.
So then I thought, what is Debug
if not just another serialisation format? Moreover, looking at the advanced API, it's easy to see how DebugMap
is similar to SerializeMap
or DebugTuple
to SerializeTuple
and so on.
So yes, I've decided to wrap all these std::fmt
structures into newtypes that implement serde::ser
traits, and then to implement main Serializer
interface that would call out to them simulating exact same output that #[derive(Debug)]
would normally produce for you with its own auto-generated code, unless you enabled some of the serde
-specific attributes to control serialisation behaviour.
How do I use it? #
On any types where you already use #[derive(Debug)]
, you can replace with #[derive(SerDebug)]
and add serde-provided #[derive(Serialize)]
if you don't already have it.
As mentioned above, by default the output is aimed to exactly match the built-in derive, and that's not very interesting to observe, so instead, let's add a bunch of serde
attributes to customise it (usually you won't need that many, this is done purely for demonstration purposes):
#[derive(Serialize, SerDebug)]
pub enum MyEnum {
// renaming items works as expected
#[serde(rename = "AAAAAAA!!!")]
A,
B(u32),
C { flag: bool },
}
#[derive(Serialize, SerDebug)]
// so does bulk rename on containers
#[serde(rename_all = "PascalCase")]
pub struct MyStruct {
number: u32,
my_enum: Vec<MyEnum>,
// we might want to hide some items from the output
#[serde(skip_serializing)]
hidden: bool,
// or override serialisation for otherwise verbose wrappers or
// third-party types that don't implement `Debug` and/or `Serialize`
#[serde(serialize_with = "custom_serialize")]
custom_type: CustomType,
}
fn custom_serialize<S: serde::Serializer>(value: &CustomType, ser: S) -> Result<S::Ok, S::Error> {
use serde::Serialize;
value.0.serialize(ser)
}
Now, if we try to define a sample MyStruct
value
let s = MyStruct {
number: 42,
my_enum: vec![MyEnum::A, MyEnum::B(10), MyEnum::C { flag: true }],
hidden: true,
custom_type: CustomType(20),
};
and print its debug representation as we normally would
println!("{:#?}", s);
you will see that the output still resembles Debug
but has our customisations applied:
MyStruct {
Number: 42,
MyEnum: [
AAAAAAA!!!,
B(
10
),
C {
flag: true
}
],
CustomType: 20
}
Why you might not want it #
serde
, especially serde_derive
, might be a heavy dependency to add to your project as it sometimes adds non-trivial overhead to compilation times.
On average-sized and larger projects, chances are that you already have it somewhere in your tree and use for other (de)serialisation targets. In that case the difference should be negligible as now you will need to compile just one more custom derive (which you will need anyway if you decided to customise your output with one of the helper crates). However, if you do not use serde yet, you might be better off with derivative
helper - I haven't personally compared compilation times, but I do think that it will be faster than all of serde
+ serde_derive
+ serdebug
combined.
This is it for now, but if you find any bugs or have improvement suggestions which are specific to serdebug
and not serde
in general, please do report them at github issue tracker.
More posts: