Repository: jonasbb/serde_with
Pull request #763 (2024-07-03 20:52:51)

Implement `MapSkipError`, analogous to `VecSkipError`, but for map-like data structures.
Currently, there is a `VecSkipError` for resiliently handling heterogenous sequences. However, there are also heterogenous maps in the wild, notably the bundle version information plist from Apple.

This PR implements `MapSkipError`, analogous to `VecSkipError`, deserializes an entry only if both the key and value are deserializable.

## Notable Changes
- The impl of `VecSkipError` is moved to a new file `de/skip_error.rs` and `ser/skip_error.rs` to reuse `GoodOrError` with `MapSkipError`.
- The impl of `MapSkipError` works for all the maps enumerated by `impls::macros::foreach_map`.

## Unanswered Questions
- [x] Do we also need to implement this for kv-tuple lists?
- [x] Do we also need to implement modules under `serde_with::rust` for `BTreeMap` and `HashMap`?

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jonasbb/serde_with/pull/763?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) Report
Attention: Patch coverage is `73.52941%` with `9 lines` in your changes missing coverage. Please review.
> Project coverage is 67.26%. Comparing base [(`dee706a`)](https://app.codecov.io/gh/jonasbb/serde_with/commit/dee706a528cb37f853be5bd2b6b4c2b3605eab80?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) to head [(`275fe7f`)](https://app.codecov.io/gh/jonasbb/serde_with/commit/275fe7fdfbe3a149895cb49d0183bf94fd8886a4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart).
> Report is 3 commits behind head on master.

| [Files](https://app.codecov.io/gh/jonasbb/serde_with/pull/763?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) | Patch % | Lines |
|---|---|---|
| [serde\_with/src/de/skip\_error.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/763?src=pr&el=tree&filepath=serde_with%2Fsrc%2Fde%2Fskip_error.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC9zcmMvZGUvc2tpcF9lcnJvci5ycw==) | 70.00% | [9 Missing :warning: ](https://app.codecov.io/gh/jonasbb/serde_with/pull/763?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #763      +/-   ##
==========================================
+ Coverage   67.25%   67.26%   +0.01%     
==========================================
  Files          38       40       +2     
  Lines        2452     2465      +13     
==========================================
+ Hits         1649     1658       +9     
- Misses        803      807       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/jonasbb/serde_with/pull/763?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart).

jonasbb: Thanks for the PR, this looks all really good.

Regarding your questions:
1. It is not necessary to support kv-list. You can add support if you want, but this PR is fine to merge as is.
2. No, you shouldn't add more to `serde_with::rust`. The `VecSkipError` doesn't have any other variations either. In general, it is difficult to combine such different behaviors and if you combine too many you have a giant compatibility matrix.
johnmave126: Cooool.

You are right. Under the current way of enumerating different types of map, adding support to kv-list probably means adding support for `MapPreventDuplicates` and alike, which is out of the scope of this PR. I don't plan to further change this PR then.

Patch:
diff --git a/serde_with/src/de/impls.rs b/serde_with/src/de/impls.rs
index 6b83ca3..de00509 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<T>> for VecSkipError<U>
-where
-    U: DeserializeAs<'de, T>,
-{
-    fn deserialize_as<D>(deserializer: D) -> Result<Vec<T>, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        enum GoodOrError<T, TAs> {
-            Good(T),
-            // Only here to consume the TAs generic
-            Error(PhantomData<TAs>),
-        }
-
-        impl<'de, T, TAs> Deserialize<'de> for GoodOrError<T, TAs>
-        where
-            TAs: DeserializeAs<'de, T>,
-        {
-            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                let is_hr = deserializer.is_human_readable();
-                let content: content::de::Content<'de> = Deserialize::deserialize(deserializer)?;
-
-                Ok(
-                    match <DeserializeAsWrap<T, TAs>>::deserialize(
-                        content::de::ContentDeserializer::<D::Error>::new(content, is_hr),
-                    ) {
-                        Ok(elem) => GoodOrError::Good(elem.into_inner()),
-                        Err(_) => GoodOrError::Error(PhantomData),
-                    },
-                )
-            }
-        }
-
-        struct SeqVisitor<T, U> {
-            marker: PhantomData<T>,
-            marker2: PhantomData<U>,
-        }
-
-        impl<'de, T, TAs> Visitor<'de> for SeqVisitor<T, TAs>
-        where
-            TAs: DeserializeAs<'de, T>,
-        {
-            type Value = Vec<T>;
-
-            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
-                formatter.write_str("a sequence")
-            }
-
-            fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
-            where
-                A: SeqAccess<'de>,
-            {
-                utils::SeqIter::new(seq)
-                    .filter_map(|res: Result<GoodOrError<T, TAs>, A::Error>| match res {
-                        Ok(GoodOrError::Good(value)) => Some(Ok(value)),
-                        Ok(GoodOrError::Error(_)) => None,
-                        Err(err) => Some(Err(err)),
-                    })
-                    .collect()
-            }
-        }
-
-        let visitor = SeqVisitor::<T, U> {
-            marker: PhantomData,
-            marker2: PhantomData,
-        };
-        deserializer.deserialize_seq(visitor)
-    }
-}
-
 impl<'de, Str> DeserializeAs<'de, Option<Str>> for NoneAsEmptyString
 where
     Str: FromStr,
diff --git a/serde_with/src/de/mod.rs b/serde_with/src/de/mod.rs
index 984950c..c566f37 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 0000000..e6b915d
--- /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<T, TAs> {
+    Good(T),
+    // Only here to consume the TAs generic
+    Error(PhantomData<TAs>),
+}
+
+impl<'de, T, TAs> Deserialize<'de> for GoodOrError<T, TAs>
+where
+    TAs: DeserializeAs<'de, T>,
+{
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let is_hr = deserializer.is_human_readable();
+        let content: content::de::Content<'de> = Deserialize::deserialize(deserializer)?;
+
+        Ok(
+            match <DeserializeAsWrap<T, TAs>>::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<T>> for VecSkipError<U>
+where
+    U: DeserializeAs<'de, T>,
+{
+    fn deserialize_as<D>(deserializer: D) -> Result<Vec<T>, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct SeqVisitor<T, U> {
+            marker: PhantomData<T>,
+            marker2: PhantomData<U>,
+        }
+
+        impl<'de, T, TAs> Visitor<'de> for SeqVisitor<T, TAs>
+        where
+            TAs: DeserializeAs<'de, T>,
+        {
+            type Value = Vec<T>;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+                formatter.write_str("a sequence")
+            }
+
+            fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
+            where
+                A: SeqAccess<'de>,
+            {
+                utils::SeqIter::new(seq)
+                    .filter_map(|res: Result<GoodOrError<T, TAs>, A::Error>| match res {
+                        Ok(GoodOrError::Good(value)) => Some(Ok(value)),
+                        Ok(GoodOrError::Error(_)) => None,
+                        Err(err) => Some(Err(err)),
+                    })
+                    .collect()
+            }
+        }
+
+        let visitor = SeqVisitor::<T, U> {
+            marker: PhantomData,
+            marker2: PhantomData,
+        };
+        deserializer.deserialize_seq(visitor)
+    }
+}
+
+struct MapSkipErrorVisitor<MAP, K, KAs, V, VAs>(PhantomData<(MAP, K, KAs, V, VAs)>);
+
+impl<'de, MAP, K, KAs, V, VAs> Visitor<'de> for MapSkipErrorVisitor<MAP, K, KAs, V, VAs>
+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<A>(self, access: A) -> Result<Self::Value, A::Error>
+    where
+        A: MapAccess<'de>,
+    {
+        type KVPair<K, KAs, V, VAs> = (GoodOrError<K, KAs>, GoodOrError<V, VAs>);
+        utils::MapIter::new(access)
+            .filter_map(|res: Result<KVPair<K, KAs, V, VAs>, 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<K, V $(, $typaram)*>>
+            for MapSkipError<KAs, VAs>
+        where
+            KAs: DeserializeAs<'de, K>,
+            VAs: DeserializeAs<'de, V>,
+            $(K: $kbound1 $(+ $kbound2)*,)?
+            $($typaram: $bound1 $(+ $bound2)*),*
+        {
+            fn deserialize_as<D>(deserializer: D) -> Result<$ty<K, V $(, $typaram)*>, D::Error>
+            where
+                D: Deserializer<'de>,
+            {
+                deserializer.deserialize_map(MapSkipErrorVisitor::<
+                    $ty<K, V $(, $typaram)*>,
+                    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 6768fa3..a417d56 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<Color>,
 // => 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<DisplayFromStr, _>")]
+value: BTreeMap<u32, String>,
+
+// 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 70b5cd2..c64f1f6 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<T>(PhantomData<T>);
 
+/// 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<u16>,
+///     #[serde_as(as = "MapSkipError<DisplayFromStr, _>")]
+///     #[serde(flatten)]
+///     names: BTreeMap<u16, String>,
+/// }
+///
+/// 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<K, V>(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 e42b913..03ae0b3 100644
--- a/serde_with/src/ser/impls.rs
+++ b/serde_with/src/ser/impls.rs
@@ -559,19 +559,6 @@ where
     }
 }
 
-#[cfg(feature = "alloc")]
-impl<T, U> SerializeAs<Vec<T>> for VecSkipError<U>
-where
-    U: SerializeAs<T>,
-{
-    fn serialize_as<S>(source: &Vec<T>, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        Vec::<U>::serialize_as(source, serializer)
-    }
-}
-
 impl<T> SerializeAs<Option<T>> for NoneAsEmptyString
 where
     T: Display,
diff --git a/serde_with/src/ser/mod.rs b/serde_with/src/ser/mod.rs
index 8c599d4..42895b8 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 0000000..80066dd
--- /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<T, U> SerializeAs<Vec<T>> for VecSkipError<U>
+where
+    U: SerializeAs<T>,
+{
+    fn serialize_as<S>(source: &Vec<T>, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        Vec::<U>::serialize_as(source, serializer)
+    }
+}
+
+macro_rules! map_skip_error_handling {
+    ($tyorig:ident < K, V $(, $typaram:ident : $bound:ident)* >) => {
+        impl<K, KAs, V, VAs $(, $typaram)*> SerializeAs<$tyorig<K, V $(, $typaram)*>> for MapSkipError<KAs, VAs>
+        where
+            KAs: SerializeAs<K>,
+            VAs: SerializeAs<V>,
+            $($typaram: ?Sized + $bound,)*
+        {
+            fn serialize_as<S>(value: &$tyorig<K, V $(, $typaram)*>, serializer: S) -> Result<S::Ok, S::Error>
+            where
+                S: Serializer,
+            {
+                <$tyorig<KAs, VAs $(, $typaram)*>>::serialize_as(value, serializer)
+            }
+        }
+    }
+}
+foreach_map!(map_skip_error_handling);


Test patch:
diff --git a/serde_with/tests/hashbrown_0_14.rs b/serde_with/tests/hashbrown_0_14.rs
index 2f1267e..57c06ea 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<DisplayFromStr, _>")]
+        values: HashMap<u8, u8>,
+    }
+
+    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::<S>(
+        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<DisplayFromStr, _>")]
+        #[serde(flatten)]
+        values: HashMap<u8, u8>,
+    }
+
+    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 afb7ce8..10d92db 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<DisplayFromStr, _>")]
+        values: IndexMap<u8, u8>,
+    }
+
+    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::<S>(
+        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<DisplayFromStr, _>")]
+        #[serde(flatten)]
+        values: IndexMap<u8, u8>,
+    }
+
+    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 61edb10..0128825 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<DisplayFromStr, _>")]
+        values: IndexMap<u8, u8>,
+    }
+
+    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::<S>(
+        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<DisplayFromStr, _>")]
+        #[serde(flatten)]
+        values: IndexMap<u8, u8>,
+    }
+
+    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 c728cee..74cf32b 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::<S>(
+        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<DisplayFromStr, _>")]
+        values: BTreeMap<u8, u8>,
+    }
+
+    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::<S>(
+        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<DisplayFromStr, _>")]
+        #[serde(flatten)]
+        values: BTreeMap<u8, u8>,
+    }
+
+    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<DisplayFromStr, _>")]
+        values: HashMap<u8, u8>,
+    }
+
+    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::<S>(
+        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<DisplayFromStr, _>")]
+        #[serde(flatten)]
+        values: HashMap<u8, u8>,
+    }
+
+    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]