diff --git a/serde_with/src/de/impls.rs b/serde_with/src/de/impls.rs index 6b83ca3b..de00509a 100644 --- a/serde_with/src/de/impls.rs +++ b/serde_with/src/de/impls.rs @@ -908,80 +908,6 @@ where } } -#[cfg(feature = "alloc")] -impl<'de, T, U> DeserializeAs<'de, Vec> for VecSkipError -where - U: DeserializeAs<'de, T>, -{ - fn deserialize_as(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - enum GoodOrError { - Good(T), - // Only here to consume the TAs generic - Error(PhantomData), - } - - impl<'de, T, TAs> Deserialize<'de> for GoodOrError - where - TAs: DeserializeAs<'de, T>, - { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let is_hr = deserializer.is_human_readable(); - let content: content::de::Content<'de> = Deserialize::deserialize(deserializer)?; - - Ok( - match >::deserialize( - content::de::ContentDeserializer::::new(content, is_hr), - ) { - Ok(elem) => GoodOrError::Good(elem.into_inner()), - Err(_) => GoodOrError::Error(PhantomData), - }, - ) - } - } - - struct SeqVisitor { - marker: PhantomData, - marker2: PhantomData, - } - - impl<'de, T, TAs> Visitor<'de> for SeqVisitor - where - TAs: DeserializeAs<'de, T>, - { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("a sequence") - } - - fn visit_seq(self, seq: A) -> Result - where - A: SeqAccess<'de>, - { - utils::SeqIter::new(seq) - .filter_map(|res: Result, A::Error>| match res { - Ok(GoodOrError::Good(value)) => Some(Ok(value)), - Ok(GoodOrError::Error(_)) => None, - Err(err) => Some(Err(err)), - }) - .collect() - } - } - - let visitor = SeqVisitor:: { - marker: PhantomData, - marker2: PhantomData, - }; - deserializer.deserialize_seq(visitor) - } -} - impl<'de, Str> DeserializeAs<'de, Option> for NoneAsEmptyString where Str: FromStr, diff --git a/serde_with/src/de/mod.rs b/serde_with/src/de/mod.rs index 984950cb..c566f371 100644 --- a/serde_with/src/de/mod.rs +++ b/serde_with/src/de/mod.rs @@ -10,6 +10,8 @@ #[cfg(feature = "alloc")] mod duplicates; mod impls; +#[cfg(feature = "alloc")] +mod skip_error; use crate::prelude::*; diff --git a/serde_with/src/de/skip_error.rs b/serde_with/src/de/skip_error.rs new file mode 100644 index 00000000..e6b915d7 --- /dev/null +++ b/serde_with/src/de/skip_error.rs @@ -0,0 +1,144 @@ +use super::impls::macros::foreach_map; +use crate::prelude::*; + +#[cfg(feature = "hashbrown_0_14")] +use hashbrown_0_14::HashMap as HashbrownMap014; +#[cfg(feature = "indexmap_1")] +use indexmap_1::IndexMap; +#[cfg(feature = "indexmap_2")] +use indexmap_2::IndexMap as IndexMap2; + +enum GoodOrError { + Good(T), + // Only here to consume the TAs generic + Error(PhantomData), +} + +impl<'de, T, TAs> Deserialize<'de> for GoodOrError +where + TAs: DeserializeAs<'de, T>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let is_hr = deserializer.is_human_readable(); + let content: content::de::Content<'de> = Deserialize::deserialize(deserializer)?; + + Ok( + match >::deserialize(content::de::ContentDeserializer::< + D::Error, + >::new(content, is_hr)) + { + Ok(elem) => GoodOrError::Good(elem.into_inner()), + Err(_) => GoodOrError::Error(PhantomData), + }, + ) + } +} + +impl<'de, T, U> DeserializeAs<'de, Vec> for VecSkipError +where + U: DeserializeAs<'de, T>, +{ + fn deserialize_as(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct SeqVisitor { + marker: PhantomData, + marker2: PhantomData, + } + + impl<'de, T, TAs> Visitor<'de> for SeqVisitor + where + TAs: DeserializeAs<'de, T>, + { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a sequence") + } + + fn visit_seq(self, seq: A) -> Result + where + A: SeqAccess<'de>, + { + utils::SeqIter::new(seq) + .filter_map(|res: Result, A::Error>| match res { + Ok(GoodOrError::Good(value)) => Some(Ok(value)), + Ok(GoodOrError::Error(_)) => None, + Err(err) => Some(Err(err)), + }) + .collect() + } + } + + let visitor = SeqVisitor:: { + marker: PhantomData, + marker2: PhantomData, + }; + deserializer.deserialize_seq(visitor) + } +} + +struct MapSkipErrorVisitor(PhantomData<(MAP, K, KAs, V, VAs)>); + +impl<'de, MAP, K, KAs, V, VAs> Visitor<'de> for MapSkipErrorVisitor +where + MAP: FromIterator<(K, V)>, + KAs: DeserializeAs<'de, K>, + VAs: DeserializeAs<'de, V>, +{ + type Value = MAP; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a map") + } + + #[inline] + fn visit_map(self, access: A) -> Result + where + A: MapAccess<'de>, + { + type KVPair = (GoodOrError, GoodOrError); + utils::MapIter::new(access) + .filter_map(|res: Result, A::Error>| match res { + Ok((GoodOrError::Good(key), GoodOrError::Good(value))) => Some(Ok((key, value))), + Ok(_) => None, + Err(err) => Some(Err(err)), + }) + .collect() + } +} + +#[cfg(feature = "alloc")] +macro_rules! map_impl { + ( + $ty:ident < K $(: $kbound1:ident $(+ $kbound2:ident)*)?, V $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)*)* >, + $with_capacity:expr + ) => { + impl<'de, K, V, KAs, VAs $(, $typaram)*> DeserializeAs<'de, $ty> + for MapSkipError + where + KAs: DeserializeAs<'de, K>, + VAs: DeserializeAs<'de, V>, + $(K: $kbound1 $(+ $kbound2)*,)? + $($typaram: $bound1 $(+ $bound2)*),* + { + fn deserialize_as(deserializer: D) -> Result<$ty, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(MapSkipErrorVisitor::< + $ty, + K, + KAs, + V, + VAs, + >(PhantomData)) + } + } + }; +} +foreach_map!(map_impl); diff --git a/serde_with/src/guide/serde_as_transformations.md b/serde_with/src/guide/serde_as_transformations.md index 6768fa3c..a417d560 100644 --- a/serde_with/src/guide/serde_as_transformations.md +++ b/serde_with/src/guide/serde_as_transformations.md @@ -11,26 +11,27 @@ This page lists the transformations implemented in this crate and supported by ` 7. [Convert to an intermediate type using `TryInto`](#convert-to-an-intermediate-type-using-tryinto) 8. [`Default` from `null`](#default-from-null) 9. [De/Serialize into `Vec`, ignoring errors](#deserialize-into-vec-ignoring-errors) -10. [De/Serialize with `FromStr` and `Display`](#deserialize-with-fromstr-and-display) -11. [`Duration` as seconds](#duration-as-seconds) -12. [Hex encode bytes](#hex-encode-bytes) -13. [Ignore deserialization errors](#ignore-deserialization-errors) -14. [`Maps` to `Vec` of enums](#maps-to-vec-of-enums) -15. [`Maps` to `Vec` of tuples](#maps-to-vec-of-tuples) -16. [`NaiveDateTime` like UTC timestamp](#naivedatetime-like-utc-timestamp) -17. [`None` as empty `String`](#none-as-empty-string) -18. [One or many elements into `Vec`](#one-or-many-elements-into-vec) -19. [Overwrite existing set values](#overwrite-existing-set-values) -20. [Pick first successful deserialization](#pick-first-successful-deserialization) -21. [Prefer the first map key when duplicates exist](#prefer-the-first-map-key-when-duplicates-exist) -22. [Prevent duplicate map keys](#prevent-duplicate-map-keys) -23. [Prevent duplicate set values](#prevent-duplicate-set-values) -24. [Struct fields as map keys](#struct-fields-as-map-keys) -25. [Timestamps as seconds since UNIX epoch](#timestamps-as-seconds-since-unix-epoch) -26. [Value into JSON String](#value-into-json-string) -27. [`Vec` of tuples to `Maps`](#vec-of-tuples-to-maps) -28. [Well-known time formats for `OffsetDateTime`](#well-known-time-formats-for-offsetdatetime) -29. [De/Serialize depending on `De/Serializer::is_human_readable`](#deserialize-depending-on-deserializeris_human_readable) +10. [De/Serialize into a map, ignoring errors](#deserialize-into-a-map-ignoring-errors) +11. [De/Serialize with `FromStr` and `Display`](#deserialize-with-fromstr-and-display) +12. [`Duration` as seconds](#duration-as-seconds) +13. [Hex encode bytes](#hex-encode-bytes) +14. [Ignore deserialization errors](#ignore-deserialization-errors) +15. [`Maps` to `Vec` of enums](#maps-to-vec-of-enums) +16. [`Maps` to `Vec` of tuples](#maps-to-vec-of-tuples) +17. [`NaiveDateTime` like UTC timestamp](#naivedatetime-like-utc-timestamp) +18. [`None` as empty `String`](#none-as-empty-string) +19. [One or many elements into `Vec`](#one-or-many-elements-into-vec) +20. [Overwrite existing set values](#overwrite-existing-set-values) +21. [Pick first successful deserialization](#pick-first-successful-deserialization) +22. [Prefer the first map key when duplicates exist](#prefer-the-first-map-key-when-duplicates-exist) +23. [Prevent duplicate map keys](#prevent-duplicate-map-keys) +24. [Prevent duplicate set values](#prevent-duplicate-set-values) +25. [Struct fields as map keys](#struct-fields-as-map-keys) +26. [Timestamps as seconds since UNIX epoch](#timestamps-as-seconds-since-unix-epoch) +27. [Value into JSON String](#value-into-json-string) +28. [`Vec` of tuples to `Maps`](#vec-of-tuples-to-maps) +29. [Well-known time formats for `OffsetDateTime`](#well-known-time-formats-for-offsetdatetime) +30. [De/Serialize depending on `De/Serializer::is_human_readable`](#deserialize-depending-on-deserializeris_human_readable) ## Base64 encode bytes @@ -181,6 +182,25 @@ colors: Vec, // => vec![Blue, Green] ``` +## De/Serialize into a map, ignoring errors + +[`MapSkipError`] + +For formats with heterogeneously typed maps, we can collect only the elements where both key and value are deserializable. +This is also useful in conjunction to `#[serde(flatten)]` to ignore some entries when capturing additional fields. + +```ignore +// JSON +"value": {"0": "v0", "5": "v5", "str": "str", "10": 2}, + +// Rust +#[serde_as(as = "MapSkipError")] +value: BTreeMap, + +// Only deserializes entries with a numerical key and a string value, i.e., +{0 => "v0", 5 => "v5"} +``` + ## De/Serialize with `FromStr` and `Display` Useful if a type implements `FromStr` / `Display` but not `Deserialize` / `Serialize`. @@ -614,3 +634,4 @@ value: u128, [`TimestampSecondsWithFrac`]: crate::TimestampSecondsWithFrac [`TryFromInto`]: crate::TryFromInto [`VecSkipError`]: crate::VecSkipError +[`MapSkipError`]: crate::MapSkipError diff --git a/serde_with/src/lib.rs b/serde_with/src/lib.rs index 70b5cd28..c64f1f6a 100644 --- a/serde_with/src/lib.rs +++ b/serde_with/src/lib.rs @@ -2205,6 +2205,64 @@ pub struct BorrowCow; #[cfg(feature = "alloc")] pub struct VecSkipError(PhantomData); +/// Deserialize a map, skipping keys and values which fail to deserialize. +/// +/// By default serde terminates if it fails to deserialize a key or a value when deserializing +/// a map. Sometimes a map has heterogeneous keys or values but we only care about some specific +/// types, and it is desirable to skip entries on errors. +/// +/// It is especially useful in conjunction to `#[serde(flatten)]` to capture a map mixed in with +/// other entries which we don't want to exhaust in the type definition. +/// +/// The serialization behavior is identical to the underlying map. +/// +/// The implementation supports both the [`HashMap`] and the [`BTreeMap`] from the standard library. +/// +/// [`BTreeMap`]: std::collections::BTreeMap +/// [`HashMap`]: std::collections::HashMap +/// +/// # Examples +/// +/// ```rust +/// # #[cfg(feature = "macros")] { +/// # use serde::{Deserialize, Serialize}; +/// # use std::collections::BTreeMap; +/// # use serde_with::{serde_as, DisplayFromStr, MapSkipError}; +/// # +/// #[serde_as] +/// # #[derive(Debug, PartialEq)] +/// #[derive(Deserialize, Serialize)] +/// struct VersionNames { +/// yanked: Vec, +/// #[serde_as(as = "MapSkipError")] +/// #[serde(flatten)] +/// names: BTreeMap, +/// } +/// +/// let data = VersionNames { +/// yanked: vec![2, 5], +/// names: BTreeMap::from_iter([ +/// (0u16, "v0".to_string()), +/// (1, "v1".to_string()), +/// (4, "v4".to_string()) +/// ]), +/// }; +/// let source_json = r#"{ +/// "0": "v0", +/// "1": "v1", +/// "4": "v4", +/// "yanked": [2, 5], +/// "last_updated": 1704085200 +/// }"#; +/// let data_json = r#"{"yanked":[2,5],"0":"v0","1":"v1","4":"v4"}"#; +/// // Ensure serialization and deserialization produce the expected results +/// assert_eq!(data_json, serde_json::to_string(&data).unwrap()); +/// assert_eq!(data, serde_json::from_str(source_json).unwrap()); +/// # } +/// ``` +#[cfg(feature = "alloc")] +pub struct MapSkipError(PhantomData<(K, V)>); + /// Deserialize a boolean from a number /// /// Deserialize a number (of `u8`) and turn it into a boolean. diff --git a/serde_with/src/ser/impls.rs b/serde_with/src/ser/impls.rs index e42b9139..03ae0b3d 100644 --- a/serde_with/src/ser/impls.rs +++ b/serde_with/src/ser/impls.rs @@ -559,19 +559,6 @@ where } } -#[cfg(feature = "alloc")] -impl SerializeAs> for VecSkipError -where - U: SerializeAs, -{ - fn serialize_as(source: &Vec, serializer: S) -> Result - where - S: Serializer, - { - Vec::::serialize_as(source, serializer) - } -} - impl SerializeAs> for NoneAsEmptyString where T: Display, diff --git a/serde_with/src/ser/mod.rs b/serde_with/src/ser/mod.rs index 8c599d4b..42895b81 100644 --- a/serde_with/src/ser/mod.rs +++ b/serde_with/src/ser/mod.rs @@ -10,6 +10,8 @@ #[cfg(feature = "alloc")] mod duplicates; mod impls; +#[cfg(feature = "alloc")] +mod skip_error; use crate::prelude::*; diff --git a/serde_with/src/ser/skip_error.rs b/serde_with/src/ser/skip_error.rs new file mode 100644 index 00000000..80066dd2 --- /dev/null +++ b/serde_with/src/ser/skip_error.rs @@ -0,0 +1,40 @@ +use super::impls::macros::foreach_map; +use crate::prelude::*; + +#[cfg(feature = "hashbrown_0_14")] +use hashbrown_0_14::HashMap as HashbrownMap014; +#[cfg(feature = "indexmap_1")] +use indexmap_1::IndexMap; +#[cfg(feature = "indexmap_2")] +use indexmap_2::IndexMap as IndexMap2; + +impl SerializeAs> for VecSkipError +where + U: SerializeAs, +{ + fn serialize_as(source: &Vec, serializer: S) -> Result + where + S: Serializer, + { + Vec::::serialize_as(source, serializer) + } +} + +macro_rules! map_skip_error_handling { + ($tyorig:ident < K, V $(, $typaram:ident : $bound:ident)* >) => { + impl SerializeAs<$tyorig> for MapSkipError + where + KAs: SerializeAs, + VAs: SerializeAs, + $($typaram: ?Sized + $bound,)* + { + fn serialize_as(value: &$tyorig, serializer: S) -> Result + where + S: Serializer, + { + <$tyorig>::serialize_as(value, serializer) + } + } + } +} +foreach_map!(map_skip_error_handling); diff --git a/serde_with/tests/hashbrown_0_14.rs b/serde_with/tests/hashbrown_0_14.rs index 2f1267e8..57c06eae 100644 --- a/serde_with/tests/hashbrown_0_14.rs +++ b/serde_with/tests/hashbrown_0_14.rs @@ -291,3 +291,96 @@ fn prohibit_duplicate_value_hashset() { expect![[r#"invalid entry: found duplicate value at line 1 column 15"#]], ); } + +#[test] +fn test_map_skip_error_hashmap() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + values: HashMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "values": { + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + } + }"#, + ); + check_error_deserialization::( + r#"{"tag":"type", "values":{"0": 1,}}"#, + expect!["trailing comma at line 1 column 33"], + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "values": { + "255": 255, + "0": 0 + } + }"#]], + ); +} + +#[test] +fn test_map_skip_error_hashmap_flatten() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + #[serde(flatten)] + values: HashMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + }"#, + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "255": 255, + "0": 0 + }"#]], + ); +} diff --git a/serde_with/tests/indexmap_1.rs b/serde_with/tests/indexmap_1.rs index afb7ce82..10d92dbc 100644 --- a/serde_with/tests/indexmap_1.rs +++ b/serde_with/tests/indexmap_1.rs @@ -251,3 +251,96 @@ fn prohibit_duplicate_value_indexset() { expect![[r#"invalid entry: found duplicate value at line 1 column 15"#]], ); } + +#[test] +fn test_map_skip_error_indexmap() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + values: IndexMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "values": { + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + } + }"#, + ); + check_error_deserialization::( + r#"{"tag":"type", "values":{"0": 1,}}"#, + expect!["trailing comma at line 1 column 33"], + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "values": { + "0": 0, + "255": 255 + } + }"#]], + ); +} + +#[test] +fn test_map_skip_error_indexmap_flatten() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + #[serde(flatten)] + values: IndexMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + }"#, + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "0": 0, + "255": 255 + }"#]], + ); +} diff --git a/serde_with/tests/indexmap_2.rs b/serde_with/tests/indexmap_2.rs index 61edb10a..0128825c 100644 --- a/serde_with/tests/indexmap_2.rs +++ b/serde_with/tests/indexmap_2.rs @@ -251,3 +251,96 @@ fn prohibit_duplicate_value_indexset() { expect![[r#"invalid entry: found duplicate value at line 1 column 15"#]], ); } + +#[test] +fn test_map_skip_error_indexmap() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + values: IndexMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "values": { + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + } + }"#, + ); + check_error_deserialization::( + r#"{"tag":"type", "values":{"0": 1,}}"#, + expect!["trailing comma at line 1 column 33"], + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "values": { + "0": 0, + "255": 255 + } + }"#]], + ); +} + +#[test] +fn test_map_skip_error_indexmap_flatten() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + #[serde(flatten)] + values: IndexMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + }"#, + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "0": 0, + "255": 255 + }"#]], + ); +} diff --git a/serde_with/tests/serde_as/lib.rs b/serde_with/tests/serde_as/lib.rs index c728ceea..74cf32b4 100644 --- a/serde_with/tests/serde_as/lib.rs +++ b/serde_with/tests/serde_as/lib.rs @@ -572,6 +572,10 @@ fn test_vec_skip_error() { }, r#"{"tag":"type","values":[0, "str", 1, [10, 11], -2, {}, 300]}"#, ); + check_error_deserialization::( + r#"{"tag":"type", "values":[0, "str", 1, , 300]}"#, + expect!["expected value at line 1 column 39"], + ); is_equal( S { tag: "round-trip".into(), @@ -588,6 +592,188 @@ fn test_vec_skip_error() { ); } +#[test] +fn test_map_skip_error_btreemap() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + values: BTreeMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "values": { + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + } + }"#, + ); + check_error_deserialization::( + r#"{"tag":"type", "values":{"0": 1,}}"#, + expect!["trailing comma at line 1 column 33"], + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "values": { + "0": 0, + "255": 255 + } + }"#]], + ); +} + +#[test] +fn test_map_skip_error_btreemap_flatten() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + #[serde(flatten)] + values: BTreeMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + }"#, + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "0": 0, + "255": 255 + }"#]], + ); +} + +#[test] +fn test_map_skip_error_hashmap() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + values: HashMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "values": { + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {} + } + }"#, + ); + check_error_deserialization::( + r#"{"tag":"type", "values":{"0": 1,}}"#, + expect!["trailing comma at line 1 column 33"], + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(255, 0)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "values": { + "255": 0 + } + }"#]], + ); +} + +#[test] +fn test_map_skip_error_hashmap_flatten() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + #[serde(flatten)] + values: HashMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {} + }"#, + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(255, 0)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "255": 0 + }"#]], + ); +} + #[test] fn test_serialize_reference() { #[serde_as]