Repository: jonasbb/serde_with
Pull request #833 (2025-05-01 00:23:42)

Add `SerializeDisplayAlt` macro
This PR introduces the `SerializeDisplayAlt` derive macro.

This is an alternative to the `SerializeDisplay` macro except instead of using the plain formatting like `format!("{}", ...)`, it serializes with the `Formatter::alternate` flag set to true, like `format!("{:#}", ...)`.


Patch:
diff --git a/serde_with_macros/CHANGELOG.md b/serde_with_macros/CHANGELOG.md
index 4e53548..6bb6c83 100644
--- a/serde_with_macros/CHANGELOG.md
+++ b/serde_with_macros/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 ## [Unreleased]
 
+### Added
+* Introduce `SerializeDisplayAlt` derive macro (#833)
+    An alternative to the `SerializeDisplay` macro except instead of using the
+    plain formatting like `format!("{}", ...)`, it serializes with the
+    `Formatter::alternate` flag set to true, like `format!("{:#}", ...)`
+
 ## [3.12.0] - 2024-12-25
 
 No changes.
diff --git a/serde_with_macros/src/lib.rs b/serde_with_macros/src/lib.rs
index 657cf68..6be4abb 100644
--- a/serde_with_macros/src/lib.rs
+++ b/serde_with_macros/src/lib.rs
@@ -1185,11 +1185,91 @@ pub fn derive_serialize_display(item: TokenStream) -> TokenStream {
     };
     TokenStream::from(serialize_display(
         input,
+        false,
         derive_options.get_serde_with_path(),
     ))
 }
 
-fn serialize_display(mut input: DeriveInput, serde_with_crate_path: Path) -> TokenStream2 {
+/// Serialize value by using its [`Display`] implementation with the “alternate” (`#`) format flag
+///
+/// This derive implements `serde::Serialize` for any type that already implements
+/// [`std::fmt::Display`], emitting its string form using the alternate formatting specifier
+/// (`{:#}`) instead of the normal `{}`.  In other words, rather than calling
+/// `format!("{}", self)`, it calls `format!("{:#}", self)`.
+///
+/// Ensure that your type implements [`Display`], or you will get a compile‐error such as:
+/// ```text
+/// error[E0277]: `MyType` doesn't implement `std::fmt::Display`
+/// ```
+///
+/// Deserialization from strings via [`std::str::FromStr`] is handled by the companion
+/// [`DeserializeFromStr`] derive.
+///
+/// # Attributes
+///
+/// You may customize which `serde_with` crate is used (for renamed or re-exported crates)
+/// via the same attribute namespace:
+///
+/// * `#[serde_with(crate = "...")]`  
+///   When your workspace renames or re-exports `serde_with`, use this to point at the correct path.
+///   For example:  
+///   ```rust,ignore
+///   #[derive(SerializeDisplayAlt)]
+///   #[serde_with(crate = "my_forked_serde_with")]
+///   pub struct Foo(/* … */);
+///   ```
+///
+/// # Example
+///
+/// ```rust,ignore
+/// use std::fmt;
+/// use serde_with::{SerializeDisplayAlt, DeserializeFromStr};
+///
+/// #[derive(Debug, Clone, SerializeDisplayAlt, DeserializeFromStr)]
+/// #[serde(transparent)]
+/// pub struct MyType(u32);
+///
+/// impl fmt::Display for MyType {
+///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+///         if f.alternate() {
+///             // Alternate formatting: hex with 0x prefix
+///             write!(f, "0x{:X}", self.0)
+///         } else {
+///             // Standard formatting: decimal
+///             write!(f, "{}", self.0)
+///         }
+///     }
+/// }
+///
+/// let v = MyType(15);
+/// // SerializeDisplayAlt always uses `{:#}`, so this yields `"0xF"`
+/// assert_eq!(r#""0xF""#, serde_json::to_string(&v).unwrap());
+/// ```
+///
+/// [`Display`]: std::fmt::Display  
+/// [`FromStr`]: std::str::FromStr  
+/// [`DeserializeFromStr`]: crate::DeserializeFromStr
+#[proc_macro_derive(SerializeDisplayAlt, attributes(serde_with))]
+pub fn derive_serialize_display_alt(item: TokenStream) -> TokenStream {
+    let input: DeriveInput = parse_macro_input!(item);
+    let derive_options = match DeriveOptions::from_derive_input(&input) {
+        Ok(opt) => opt,
+        Err(err) => {
+            return err;
+        }
+    };
+    TokenStream::from(serialize_display(
+        input,
+        true,
+        derive_options.get_serde_with_path(),
+    ))
+}
+
+fn serialize_display(
+    mut input: DeriveInput,
+    alternate: bool,
+    serde_with_crate_path: Path,
+) -> TokenStream2 {
     let ident = input.ident;
     input
         .generics
@@ -1197,6 +1277,13 @@ fn serialize_display(mut input: DeriveInput, serde_with_crate_path: Path) -> Tok
         .predicates
         .push(parse_quote!(Self: #serde_with_crate_path::__private__::Display));
     let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+    let collect_str_param = if alternate {
+        quote! { &format_args!("{self:#}") }
+    } else {
+        quote! { &self }
+    };
+
     quote! {
         #[automatically_derived]
         impl #impl_generics #serde_with_crate_path::serde::Serialize for #ident #ty_generics #where_clause {
@@ -1207,7 +1294,7 @@ fn serialize_display(mut input: DeriveInput, serde_with_crate_path: Path) -> Tok
             where
                 __S: #serde_with_crate_path::serde::Serializer,
             {
-                serializer.collect_str(&self)
+                serializer.collect_str(#collect_str_param)
             }
         }
     }


Test patch:
diff --git a/serde_with_test/tests/derive_display_alt_fromstr.rs b/serde_with_test/tests/derive_display_alt_fromstr.rs
new file mode 100644
index 0000000..834bf5e
--- /dev/null
+++ b/serde_with_test/tests/derive_display_alt_fromstr.rs
@@ -0,0 +1,81 @@
+//! Test that the derive macros properly name all the types and traits used
+
+// Ensure no prelude is available
+#![no_implicit_prelude]
+#![allow(dead_code)]
+
+use ::s_with::{DeserializeFromStr, SerializeDisplayAlt};
+
+#[derive(DeserializeFromStr, SerializeDisplayAlt)]
+#[serde_with(crate = "::s_with")]
+struct A;
+
+impl ::std::str::FromStr for A {
+    type Err = ::std::string::String;
+    fn from_str(_: &str) -> ::std::result::Result<Self, Self::Err> {
+        ::std::unimplemented!()
+    }
+}
+
+impl ::std::fmt::Display for A {
+    fn fmt(&self, _: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::std::unimplemented!()
+    }
+}
+
+#[derive(DeserializeFromStr, SerializeDisplayAlt)]
+#[serde_with(crate = "::s_with")]
+struct G<T>(T);
+
+impl<T> ::std::str::FromStr for G<T> {
+    type Err = ::std::string::String;
+    fn from_str(_: &str) -> ::std::result::Result<Self, Self::Err> {
+        ::std::unimplemented!()
+    }
+}
+
+impl<T> ::std::fmt::Display for G<T> {
+    fn fmt(&self, _: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::std::unimplemented!()
+    }
+}
+
+// Ensure the common 1-letter generics are not used by the derive macros
+// The macros use:
+// D: Deserializer
+// S: Serializer
+// E: Error
+// https://github.com/jonasbb/serde_with/pull/526
+#[derive(DeserializeFromStr, SerializeDisplayAlt)]
+#[serde_with(crate = "::s_with")]
+struct MoreG<D, E, S>(D, E, S);
+
+impl<D, E, S> ::std::str::FromStr for MoreG<D, E, S> {
+    type Err = ::std::string::String;
+    fn from_str(_: &str) -> ::std::result::Result<Self, Self::Err> {
+        ::std::unimplemented!()
+    }
+}
+
+impl<D, E, S> ::std::fmt::Display for MoreG<D, E, S> {
+    fn fmt(&self, _: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::std::unimplemented!()
+    }
+}
+
+#[derive(DeserializeFromStr, SerializeDisplayAlt)]
+#[serde_with(crate = "::s_with")]
+struct LT<'a>(&'a ());
+
+impl ::std::str::FromStr for LT<'_> {
+    type Err = ::std::string::String;
+    fn from_str(_: &str) -> ::std::result::Result<Self, Self::Err> {
+        ::std::unimplemented!()
+    }
+}
+
+impl ::std::fmt::Display for LT<'_> {
+    fn fmt(&self, _: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::std::unimplemented!()
+    }
+}