Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions serde_with/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions serde_with/src/de/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,45 @@ where
}
}

#[cfg(all(feature = "alloc", feature = "std"))]
impl<'de, T, TAs, FORMAT, S> DeserializeAs<'de, std::collections::HashSet<T, S>>
for OneOrMany<TAs, FORMAT>
where
T: Eq + std::hash::Hash,
S: BuildHasher + Default,
TAs: DeserializeAs<'de, T>,
FORMAT: Format,
{
fn deserialize_as<D>(deserializer: D) -> Result<std::collections::HashSet<T, S>, 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 <DeserializeAsWrap<T, TAs>>::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 <DeserializeAsWrap<HashSet<T, S>, HashSet<TAs, S>>>::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
Expand Down
2 changes: 1 addition & 1 deletion serde_with/src/schemars_0_8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
44 changes: 42 additions & 2 deletions serde_with/src/ser/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ where
S: Serializer,
{
match source.len() {
1 => SerializeAsWrap::<T, U>::new(source.iter().next().expect("Cannot be empty"))
1 => SerializeAsWrap::<T, U>::new(source.first().expect("Cannot be empty"))
.serialize(serializer),
_ => SerializeAsWrap::<Vec<T>, Vec<U>>::new(source).serialize(serializer),
}
Expand Down Expand Up @@ -976,7 +976,7 @@ where
S: Serializer,
{
match source.len() {
1 => SerializeAsWrap::<T, TAs>::new(source.iter().next().expect("Cannot be empty"))
1 => SerializeAsWrap::<T, TAs>::new(source.first().expect("Cannot be empty"))
.serialize(serializer),
_ => SerializeAsWrap::<&[T], &[TAs]>::new(&source.as_slice()).serialize(serializer),
}
Expand All @@ -997,6 +997,46 @@ where
}
}

#[cfg(all(feature = "alloc", feature = "std"))]
impl<T, U, S> SerializeAs<std::collections::HashSet<T, S>> for OneOrMany<U, formats::PreferOne>
where
T: Eq + core::hash::Hash,
U: SerializeAs<T>,
{
fn serialize_as<Ser>(
source: &std::collections::HashSet<T, S>,
serializer: Ser,
) -> Result<Ser::Ok, Ser::Error>
where
Ser: Serializer,
{
match source.len() {
1 => SerializeAsWrap::<T, U>::new(source.iter().next().expect("Cannot be empty")).serialize(serializer),
_ => SerializeAsWrap::<std::collections::HashSet<T, S>, std::collections::HashSet<U, S>>::new(source).serialize(serializer),
}
}
}

#[cfg(all(feature = "alloc", feature = "std"))]
impl<T, U, S> SerializeAs<std::collections::HashSet<T, S>> for OneOrMany<U, formats::PreferMany>
where
T: Eq + core::hash::Hash,
U: SerializeAs<T>,
{
fn serialize_as<Ser>(
source: &std::collections::HashSet<T, S>,
serializer: Ser,
) -> Result<Ser::Ok, Ser::Error>
where
Ser: Serializer,
{
SerializeAsWrap::<std::collections::HashSet<T, S>, std::collections::HashSet<U, S>>::new(
source,
)
.serialize(serializer)
}
}

#[cfg(feature = "alloc")]
impl<T, TAs1> SerializeAs<T> for PickFirst<(TAs1,)>
where
Expand Down
70 changes: 64 additions & 6 deletions serde_with/tests/serde_as/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use serde_with::{
NoneAsEmptyString, OneOrMany, Same, Seq, StringWithSeparator,
};
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
sync::{Mutex, RwLock},
};

Expand Down Expand Up @@ -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]"#);
Expand Down Expand Up @@ -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<u32>);

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::<S1>(
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<DisplayFromStr>")] HashSet<u32>);

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::<S2>(
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<u32>);

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() {
Expand Down