Repository: jonasbb/serde_with
Pull request #618 (2023-07-16 08:34:20)

Add FromIntoRef and TryFromIntoRef
Conversions from references are rare, however a user who controls the implementation of the type, can implement `Into<T> from &'a MyType` and use this to avoid cloning.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jonasbb/serde_with/pull/618?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) Report
> Merging [#618](https://app.codecov.io/gh/jonasbb/serde_with/pull/618?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) (94b0b7f) into [master](https://app.codecov.io/gh/jonasbb/serde_with/commit/e096f0b3328e6903fda5c252a0d5ad9ea5f3e402?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) (e096f0b) will **increase** coverage by `0.58%`.
> The diff coverage is `97.29%`.

```diff
@@            Coverage Diff             @@
##           master     #618      +/-   ##
==========================================
+ Coverage   63.63%   64.22%   +0.58%     
==========================================
  Files          35       36       +1     
  Lines        2090     2127      +37     
==========================================
+ Hits         1330     1366      +36     
- Misses        760      761       +1     
```


| [Impacted Files](https://app.codecov.io/gh/jonasbb/serde_with/pull/618?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) | Coverage Δ | |
|---|---|---|
| [serde\_with/src/de/impls.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/618?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC9zcmMvZGUvaW1wbHMucnM=) | `58.66% <80.00%> (+0.21%)` | :arrow_up: |
| [serde\_with/src/ser/impls.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/618?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC9zcmMvc2VyL2ltcGxzLnJz) | `91.66% <100.00%> (+0.36%)` | :arrow_up: |
| [serde\_with/tests/serde\_as/borrowfrominto.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/618?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC90ZXN0cy9zZXJkZV9hcy9ib3Jyb3dmcm9taW50by5ycw==) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart)

jonasbb: Thanks for the contribution. I think I see the utility. Can you please confirm: the new types are almost identical to `FromInto` and `TryFromInto`, except that the `SerializeAs` trait bound does not require `Clone` by instead including the reference in the `Into`/`TryInto` bounds? 
Do you have a use case for which you need this?

However, the “borrow” name part seems wrong here. In the serde ecosystem, borrow usually refers to borrowing during deserialization. The new types do not enable borrowing during serialization either, they only remove the `Clone` requirement. The reason is that `for<'a> &'a T: Into<U>` does not allow `U` to borrow from `T`. What the `for<'a>` means is any lifetime, no matter how short. Therefore, for any lifetime of `U` you can find a smaller `'a`, thus `U` cannot borrow from `T`.
I don't have any name suggestions right now. Do you have any idea?
jonasbb: Thanks for the info. I like `FromIntoRef`/`TryFromIntoRef` the most. Could you please update the name? The rest looks good. You have tests and documentation written. The only thing missing is a changelog entry, but I can create that when releasing the crate.

Patch:
diff --git a/serde_with/src/de/impls.rs b/serde_with/src/de/impls.rs
index 61eaf5c..7ea590f 100644
--- a/serde_with/src/de/impls.rs
+++ b/serde_with/src/de/impls.rs
@@ -1719,6 +1719,35 @@ where
     }
 }
 
+impl<'de, T, U> DeserializeAs<'de, T> for FromIntoRef<U>
+where
+    U: Into<T>,
+    U: Deserialize<'de>,
+{
+    fn deserialize_as<D>(deserializer: D) -> Result<T, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        Ok(U::deserialize(deserializer)?.into())
+    }
+}
+
+impl<'de, T, U> DeserializeAs<'de, T> for TryFromIntoRef<U>
+where
+    U: TryInto<T>,
+    <U as TryInto<T>>::Error: Display,
+    U: Deserialize<'de>,
+{
+    fn deserialize_as<D>(deserializer: D) -> Result<T, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        U::deserialize(deserializer)?
+            .try_into()
+            .map_err(DeError::custom)
+    }
+}
+
 #[cfg(feature = "alloc")]
 impl<'de> DeserializeAs<'de, Cow<'de, str>> for BorrowCow {
     fn deserialize_as<D>(deserializer: D) -> Result<Cow<'de, str>, D::Error>
diff --git a/serde_with/src/lib.rs b/serde_with/src/lib.rs
index 1a30b0d..6dba019 100644
--- a/serde_with/src/lib.rs
+++ b/serde_with/src/lib.rs
@@ -1825,6 +1825,87 @@ pub struct PickFirst<T>(PhantomData<T>);
 /// ```
 pub struct FromInto<T>(PhantomData<T>);
 
+/// Serialize a reference value by converting to/from a proxy type with serde support.
+///
+/// This adapter serializes a type `O` by converting it into a second type `T` and serializing `T`.
+/// Deserializing works analogue, by deserializing a `T` and then converting into `O`.
+///
+/// ```rust
+/// # #[cfg(FALSE)] {
+/// struct S {
+///     #[serde_as(as = "FromIntoRef<T>")]
+///     value: O,
+/// }
+/// # }
+/// ```
+///
+/// For serialization `O` needs to be `for<'a> &'a O: Into<T>`.
+/// For deserialization the opposite `T: Into<O>` is required.
+///
+/// **Note**: [`TryFromIntoRef`] is the more generalized version of this adapter which uses the [`TryInto`](std::convert::TryInto) trait instead.
+///
+/// # Example
+///
+/// ```rust
+/// # #[cfg(feature = "macros")] {
+/// # use serde::{Deserialize, Serialize};
+/// # use serde_json::json;
+/// # use serde_with::{serde_as, FromIntoRef};
+/// #
+/// #[derive(Debug, PartialEq)]
+/// struct Rgb {
+///     red: u8,
+///     green: u8,
+///     blue: u8,
+/// }
+///
+/// # /*
+/// impl From<(u8, u8, u8)> for Rgb { ... }
+/// impl From<Rgb> for (u8, u8, u8) { ... }
+/// # */
+/// #
+/// # impl From<(u8, u8, u8)> for Rgb {
+/// #     fn from(v: (u8, u8, u8)) -> Self {
+/// #         Rgb {
+/// #             red: v.0,
+/// #             green: v.1,
+/// #             blue: v.2,
+/// #         }
+/// #     }
+/// # }
+/// #
+/// # impl<'a> From<&'a Rgb> for (u8, u8, u8) {
+/// #     fn from(v: &'a Rgb) -> Self {
+/// #         (v.red, v.green, v.blue)
+/// #     }
+/// # }
+///
+/// #[serde_as]
+/// # #[derive(Debug, PartialEq)]
+/// #[derive(Deserialize, Serialize)]
+/// struct Color {
+///     #[serde_as(as = "FromIntoRef<(u8, u8, u8)>")]
+///     rgb: Rgb,
+/// }
+/// let color = Color {
+///     rgb: Rgb {
+///         red: 128,
+///         green: 64,
+///         blue: 32,
+///     },
+/// };
+///
+/// // Define our expected JSON form
+/// let j = json!({
+///     "rgb": [128, 64, 32],
+/// });
+/// // Ensure serialization and deserialization produce the expected results
+/// assert_eq!(j, serde_json::to_value(&color).unwrap());
+/// assert_eq!(color, serde_json::from_value(j).unwrap());
+/// # }
+/// ```
+pub struct FromIntoRef<T>(PhantomData<T>);
+
 /// Serialize value by converting to/from a proxy type with serde support.
 ///
 /// This adapter serializes a type `O` by converting it into a second type `T` and serializing `T`.
@@ -1915,6 +1996,95 @@ pub struct FromInto<T>(PhantomData<T>);
 /// ```
 pub struct TryFromInto<T>(PhantomData<T>);
 
+/// Serialize a reference value by converting to/from a proxy type with serde support.
+///
+/// This adapter serializes a type `O` by converting it into a second type `T` and serializing `T`.
+/// Deserializing works analogue, by deserializing a `T` and then converting into `O`.
+///
+/// ```rust
+/// # #[cfg(FALSE)] {
+/// struct S {
+///     #[serde_as(as = "TryFromIntoRef<T>")]
+///     value: O,
+/// }
+/// # }
+/// ```
+///
+/// For serialization `O` needs to be `for<'a> &'a O: TryInto<T>`.
+/// For deserialization the opposite `T: TryInto<O>` is required.
+/// In both cases the `TryInto::Error` type must implement [`Display`](std::fmt::Display).
+///
+/// **Note**: [`FromIntoRef`] is the more specialized version of this adapter which uses the infallible [`Into`] trait instead.
+/// [`TryFromIntoRef`] is strictly more general and can also be used where [`FromIntoRef`] is applicable.
+/// The example shows a use case, when only the deserialization behavior is fallible, but not serializing.
+///
+/// # Example
+///
+/// ```rust
+/// # #[cfg(feature = "macros")] {
+/// # use serde::{Deserialize, Serialize};
+/// # use serde_json::json;
+/// # use serde_with::{serde_as, TryFromIntoRef};
+/// # use std::convert::TryFrom;
+/// #
+/// #[derive(Debug, PartialEq)]
+/// enum Boollike {
+///     True,
+///     False,
+/// }
+///
+/// # /*
+/// impl From<Boollike> for u8 { ... }
+/// # */
+/// #
+/// impl TryFrom<u8> for Boollike {
+///     type Error = String;
+///     fn try_from(v: u8) -> Result<Self, Self::Error> {
+///         match v {
+///             0 => Ok(Boollike::False),
+///             1 => Ok(Boollike::True),
+///             _ => Err(format!("Boolikes can only be constructed from 0 or 1 but found {}", v))
+///         }
+///     }
+/// }
+/// #
+/// # impl<'a> From<&'a Boollike> for u8 {
+/// #     fn from(v: &'a Boollike) -> Self {
+/// #        match v {
+/// #            Boollike::True => 1,
+/// #            Boollike::False => 0,
+/// #        }
+/// #     }
+/// # }
+///
+/// #[serde_as]
+/// # #[derive(Debug, PartialEq)]
+/// #[derive(Deserialize, Serialize)]
+/// struct Data {
+///     #[serde_as(as = "TryFromIntoRef<u8>")]
+///     b: Boollike,
+/// }
+/// let data = Data {
+///     b: Boollike::True,
+/// };
+///
+/// // Define our expected JSON form
+/// let j = json!({
+///     "b": 1,
+/// });
+/// // Ensure serialization and deserialization produce the expected results
+/// assert_eq!(j, serde_json::to_value(&data).unwrap());
+/// assert_eq!(data, serde_json::from_value(j).unwrap());
+///
+/// // Numbers besides 0 or 1 should be an error
+/// let j = json!({
+///     "b": 2,
+/// });
+/// assert_eq!("Boolikes can only be constructed from 0 or 1 but found 2", serde_json::from_value::<Data>(j).unwrap_err().to_string());
+/// # }
+/// ```
+pub struct TryFromIntoRef<T>(PhantomData<T>);
+
 /// Borrow `Cow` data during deserialization when possible.
 ///
 /// The types `Cow<'a, [u8]>`, `Cow<'a, [u8; N]>`, and `Cow<'a, str>` can borrow from the input data during deserialization.
diff --git a/serde_with/src/ser/impls.rs b/serde_with/src/ser/impls.rs
index 0075a8b..e9e10fd 100644
--- a/serde_with/src/ser/impls.rs
+++ b/serde_with/src/ser/impls.rs
@@ -877,6 +877,36 @@ where
     }
 }
 
+impl<T, U> SerializeAs<T> for FromIntoRef<U>
+where
+    for<'a> &'a T: Into<U>,
+    U: Serialize,
+{
+    fn serialize_as<S>(source: &T, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        source.into().serialize(serializer)
+    }
+}
+
+impl<T, U> SerializeAs<T> for TryFromIntoRef<U>
+where
+    for<'a> &'a T: TryInto<U>,
+    for<'a> <&'a T as TryInto<U>>::Error: Display,
+    U: Serialize,
+{
+    fn serialize_as<S>(source: &T, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        source
+            .try_into()
+            .map_err(S::Error::custom)?
+            .serialize(serializer)
+    }
+}
+
 #[cfg(feature = "alloc")]
 impl<'a> SerializeAs<Cow<'a, str>> for BorrowCow {
     fn serialize_as<S>(source: &Cow<'a, str>, serializer: S) -> Result<S::Ok, S::Error>


Test patch:
diff --git a/serde_with/tests/serde_as/fromintoref.rs b/serde_with/tests/serde_as/fromintoref.rs
new file mode 100644
index 0000000..430986b
--- /dev/null
+++ b/serde_with/tests/serde_as/fromintoref.rs
@@ -0,0 +1,187 @@
+use super::*;
+use core::convert::TryFrom;
+use serde_with::{FromIntoRef, TryFromIntoRef};
+
+#[derive(Debug, PartialEq)]
+enum IntoSerializable {
+    A,
+    B,
+    C,
+}
+
+impl<'a> From<&'a IntoSerializable> for String {
+    fn from(value: &'a IntoSerializable) -> Self {
+        match value {
+            IntoSerializable::A => "String A",
+            IntoSerializable::B => "Some other value",
+            IntoSerializable::C => "Looks like 123",
+        }
+        .to_string()
+    }
+}
+
+#[derive(Debug, PartialEq)]
+enum FromDeserializable {
+    Zero,
+    Odd(u32),
+    Even(u32),
+}
+
+impl From<u32> for FromDeserializable {
+    fn from(value: u32) -> Self {
+        match value {
+            0 => FromDeserializable::Zero,
+            e if e % 2 == 0 => FromDeserializable::Even(e),
+            o => FromDeserializable::Odd(o),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+enum LikeBool {
+    Trueish,
+    Falseisch,
+}
+
+impl From<bool> for LikeBool {
+    fn from(b: bool) -> Self {
+        if b {
+            LikeBool::Trueish
+        } else {
+            LikeBool::Falseisch
+        }
+    }
+}
+
+impl<'a> From<&'a LikeBool> for bool {
+    fn from(lb: &'a LikeBool) -> Self {
+        match lb {
+            LikeBool::Trueish => true,
+            LikeBool::Falseisch => false,
+        }
+    }
+}
+
+#[test]
+fn test_frominto_ser() {
+    #[serde_as]
+    #[derive(Debug, PartialEq, Serialize)]
+    struct S(#[serde_as(serialize_as = "FromIntoRef<String>")] IntoSerializable);
+
+    check_serialization(S(IntoSerializable::A), expect![[r#""String A""#]]);
+    check_serialization(S(IntoSerializable::B), expect![[r#""Some other value""#]]);
+    check_serialization(S(IntoSerializable::C), expect![[r#""Looks like 123""#]]);
+}
+
+#[test]
+fn test_tryfrominto_ser() {
+    #[serde_as]
+    #[derive(Debug, PartialEq, Serialize)]
+    struct S(#[serde_as(serialize_as = "TryFromIntoRef<String>")] IntoSerializable);
+
+    check_serialization(S(IntoSerializable::A), expect![[r#""String A""#]]);
+    check_serialization(S(IntoSerializable::B), expect![[r#""Some other value""#]]);
+    check_serialization(S(IntoSerializable::C), expect![[r#""Looks like 123""#]]);
+}
+
+#[test]
+fn test_frominto_de() {
+    #[serde_as]
+    #[derive(Debug, PartialEq, Deserialize)]
+    struct S(#[serde_as(deserialize_as = "FromIntoRef<u32>")] FromDeserializable);
+
+    check_deserialization(S(FromDeserializable::Zero), "0");
+    check_deserialization(S(FromDeserializable::Odd(1)), "1");
+    check_deserialization(S(FromDeserializable::Odd(101)), "101");
+    check_deserialization(S(FromDeserializable::Even(2)), "2");
+    check_deserialization(S(FromDeserializable::Even(202)), "202");
+}
+
+#[test]
+fn test_tryfrominto_de() {
+    #[serde_as]
+    #[derive(Debug, PartialEq, Deserialize)]
+    struct S(#[serde_as(deserialize_as = "TryFromIntoRef<u32>")] FromDeserializable);
+
+    check_deserialization(S(FromDeserializable::Zero), "0");
+    check_deserialization(S(FromDeserializable::Odd(1)), "1");
+    check_deserialization(S(FromDeserializable::Odd(101)), "101");
+    check_deserialization(S(FromDeserializable::Even(2)), "2");
+    check_deserialization(S(FromDeserializable::Even(202)), "202");
+}
+
+#[test]
+fn test_frominto_de_and_ser() {
+    #[serde_as]
+    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+    struct S(#[serde_as(as = "FromIntoRef<bool>")] LikeBool);
+
+    is_equal(S(LikeBool::Trueish), expect![[r#"true"#]]);
+    is_equal(S(LikeBool::Falseisch), expect![[r#"false"#]]);
+}
+
+#[test]
+fn test_tryfrominto_de_and_ser() {
+    #[serde_as]
+    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+    struct S(#[serde_as(as = "TryFromIntoRef<bool>")] LikeBool);
+
+    is_equal(S(LikeBool::Trueish), expect![[r#"true"#]]);
+    is_equal(S(LikeBool::Falseisch), expect![[r#"false"#]]);
+}
+
+#[derive(Debug, PartialEq)]
+enum TryIntoSerializable {
+    Works,
+    Fails,
+}
+
+impl<'a> TryFrom<&'a TryIntoSerializable> for String {
+    type Error = &'static str;
+
+    fn try_from(value: &'a TryIntoSerializable) -> Result<Self, Self::Error> {
+        match value {
+            TryIntoSerializable::Works => Ok("Works".to_string()),
+            TryIntoSerializable::Fails => Err("Fails cannot be turned into String"),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+enum TryFromDeserializable {
+    Zero,
+}
+
+impl TryFrom<u32> for TryFromDeserializable {
+    type Error = &'static str;
+
+    fn try_from(value: u32) -> Result<Self, Self::Error> {
+        match value {
+            0 => Ok(TryFromDeserializable::Zero),
+            _ => Err("Number is not zero"),
+        }
+    }
+}
+
+#[test]
+fn test_tryfrominto_ser_with_error() {
+    #[serde_as]
+    #[derive(Debug, PartialEq, Serialize)]
+    struct S(#[serde_as(serialize_as = "TryFromIntoRef<String>")] TryIntoSerializable);
+
+    check_serialization(S(TryIntoSerializable::Works), expect![[r#""Works""#]]);
+    check_error_serialization(
+        S(TryIntoSerializable::Fails),
+        expect![[r#"Fails cannot be turned into String"#]],
+    );
+}
+
+#[test]
+fn test_tryfrominto_de_with_error() {
+    #[serde_as]
+    #[derive(Debug, PartialEq, Deserialize)]
+    struct S(#[serde_as(deserialize_as = "TryFromIntoRef<u32>")] TryFromDeserializable);
+
+    check_deserialization(S(TryFromDeserializable::Zero), "0");
+    check_error_deserialization::<S>("1", expect![[r#"Number is not zero"#]]);
+}
diff --git a/serde_with/tests/serde_as/lib.rs b/serde_with/tests/serde_as/lib.rs
index d9e0d3d..23e1ba8 100644
--- a/serde_with/tests/serde_as/lib.rs
+++ b/serde_with/tests/serde_as/lib.rs
@@ -12,6 +12,7 @@ mod collections;
 mod default_on;
 mod enum_map;
 mod frominto;
+mod fromintoref;
 mod key_value_map;
 mod map_tuple_list;
 mod pickfirst;