diff --git a/serde_with/CHANGELOG.md b/serde_with/CHANGELOG.md index f255881d..b274d90d 100644 --- a/serde_with/CHANGELOG.md +++ b/serde_with/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +* Support `OneOrMany` with `HashSet` + ## [3.17.0] - 2026-02-24 ### Added diff --git a/serde_with/src/de/impls.rs b/serde_with/src/de/impls.rs index d47ec5c6..25182b32 100644 --- a/serde_with/src/de/impls.rs +++ b/serde_with/src/de/impls.rs @@ -1771,6 +1771,45 @@ where } } +#[cfg(all(feature = "alloc", feature = "std"))] +impl<'de, T, TAs, FORMAT, S> DeserializeAs<'de, std::collections::HashSet> + for OneOrMany +where + T: Eq + std::hash::Hash, + S: BuildHasher + Default, + TAs: DeserializeAs<'de, T>, + FORMAT: Format, +{ + fn deserialize_as(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let is_hr = deserializer.is_human_readable(); + let content: content::de::Content<'de> = Deserialize::deserialize(deserializer)?; + + let one_err: D::Error = match >::deserialize( + content::de::ContentRefDeserializer::new(&content, is_hr), + ) { + Ok(one) => { + let mut hashset = std::collections::HashSet::default(); + hashset.insert(one.into_inner()); + return Ok(hashset); + } + Err(err) => err, + }; + let many_err: D::Error = + match , HashSet>>::deserialize( + content::de::ContentDeserializer::new(content, is_hr), + ) { + Ok(many) => return Ok(many.into_inner()), + Err(err) => err, + }; + Err(DeError::custom(format_args!( + "OneOrMany could not deserialize any variant:\n One: {one_err}\n Many: {many_err}" + ))) + } +} + #[cfg(feature = "alloc")] impl<'de, T, TAs1> DeserializeAs<'de, T> for PickFirst<(TAs1,)> where diff --git a/serde_with/src/schemars_0_8.rs b/serde_with/src/schemars_0_8.rs index 1665a2e2..6cacc6af 100644 --- a/serde_with/src/schemars_0_8.rs +++ b/serde_with/src/schemars_0_8.rs @@ -632,7 +632,7 @@ where let properties = &mut object.object().properties; for schema in one_of { if let Some(object) = schema.into_object().object { - properties.extend(object.properties.into_iter()); + properties.extend(object.properties); } } diff --git a/serde_with/src/ser/impls.rs b/serde_with/src/ser/impls.rs index f4261c18..91beeb9e 100644 --- a/serde_with/src/ser/impls.rs +++ b/serde_with/src/ser/impls.rs @@ -945,7 +945,7 @@ where S: Serializer, { match source.len() { - 1 => SerializeAsWrap::::new(source.iter().next().expect("Cannot be empty")) + 1 => SerializeAsWrap::::new(source.first().expect("Cannot be empty")) .serialize(serializer), _ => SerializeAsWrap::, Vec>::new(source).serialize(serializer), } @@ -976,7 +976,7 @@ where S: Serializer, { match source.len() { - 1 => SerializeAsWrap::::new(source.iter().next().expect("Cannot be empty")) + 1 => SerializeAsWrap::::new(source.first().expect("Cannot be empty")) .serialize(serializer), _ => SerializeAsWrap::<&[T], &[TAs]>::new(&source.as_slice()).serialize(serializer), } @@ -997,6 +997,46 @@ where } } +#[cfg(all(feature = "alloc", feature = "std"))] +impl SerializeAs> for OneOrMany +where + T: Eq + core::hash::Hash, + U: SerializeAs, +{ + fn serialize_as( + source: &std::collections::HashSet, + serializer: Ser, + ) -> Result + where + Ser: Serializer, + { + match source.len() { + 1 => SerializeAsWrap::::new(source.iter().next().expect("Cannot be empty")).serialize(serializer), + _ => SerializeAsWrap::, std::collections::HashSet>::new(source).serialize(serializer), + } + } +} + +#[cfg(all(feature = "alloc", feature = "std"))] +impl SerializeAs> for OneOrMany +where + T: Eq + core::hash::Hash, + U: SerializeAs, +{ + fn serialize_as( + source: &std::collections::HashSet, + serializer: Ser, + ) -> Result + where + Ser: Serializer, + { + SerializeAsWrap::, std::collections::HashSet>::new( + source, + ) + .serialize(serializer) + } +} + #[cfg(feature = "alloc")] impl SerializeAs for PickFirst<(TAs1,)> where diff --git a/serde_with/tests/serde_as/lib.rs b/serde_with/tests/serde_as/lib.rs index 0387818d..7e544054 100644 --- a/serde_with/tests/serde_as/lib.rs +++ b/serde_with/tests/serde_as/lib.rs @@ -35,7 +35,7 @@ use serde_with::{ NoneAsEmptyString, OneOrMany, Same, Seq, StringWithSeparator, }; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, sync::{Mutex, RwLock}, }; @@ -1183,11 +1183,11 @@ fn test_one_or_many_prefer_one() { is_equal( S1Vec(vec![1, 2, 3]), expect![[r#" - [ - 1, - 2, - 3 - ]"#]], + [ + 1, + 2, + 3 + ]"#]], ); check_deserialization(S1Vec(vec![1]), r#"1"#); check_deserialization(S1Vec(vec![1]), r#"[1]"#); @@ -1323,6 +1323,64 @@ fn test_one_or_many_prefer_many() { ); } +#[test] +fn test_one_or_many_hashset_prefer_one() { + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct S1(#[serde_as(as = "OneOrMany<_>")] HashSet); + + is_equal(S1(HashSet::new()), expect![[r#"[]"#]]); + is_equal(S1(HashSet::from([1])), expect![[r#"1"#]]); + check_deserialization(S1(HashSet::from([1])), r#"1"#); + check_deserialization(S1(HashSet::from([1])), r#"[1]"#); + check_deserialization(S1(HashSet::from([1, 2, 3])), r#"[1, 2, 3]"#); + check_deserialization(S1(HashSet::from([1, 2, 3])), r#"[3, 2, 1]"#); + check_error_deserialization::( + r#""xx""#, + expect![[r#" + OneOrMany could not deserialize any variant: + One: invalid type: string "xx", expected u32 + Many: invalid type: string "xx", expected a sequence"#]], + ); + + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct S2(#[serde_as(as = "OneOrMany")] HashSet); + + is_equal(S2(HashSet::from([1])), expect![[r#""1""#]]); + check_deserialization(S2(HashSet::from([1])), r#""1""#); + check_deserialization(S2(HashSet::from([1])), r#"["1"]"#); + check_deserialization(S2(HashSet::from([1, 2, 3])), r#"["1", "2", "3"]"#); + check_error_deserialization::( + r#"{}"#, + expect![[r#" + OneOrMany could not deserialize any variant: + One: invalid type: map, expected a string + Many: invalid type: map, expected a sequence"#]], + ); +} + +#[test] +fn test_one_or_many_hashset_prefer_many() { + use serde_with::formats::PreferMany; + + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct S1(#[serde_as(as = "OneOrMany<_, PreferMany>")] HashSet); + + is_equal(S1(HashSet::new()), expect![[r#"[]"#]]); + is_equal( + S1(HashSet::from([1])), + expect![[r#" + [ + 1 + ]"#]], + ); + check_deserialization(S1(HashSet::from([1])), r#"1"#); + check_deserialization(S1(HashSet::from([1])), r#"[1]"#); + check_deserialization(S1(HashSet::from([1, 2, 3])), r#"[1, 2, 3]"#); +} + /// Test that Cow borrows from the input #[test] fn test_borrow_cow_str() {