Repository: jonasbb/serde_with (rust)

Issue:
Hashbrown 0.15.0
Yesterday, hashbrown 0.15.0 is released.

Currently, serde_with has `hashbrown_0_14` feature which supports only 0.14.
New `hashbrown_0_15` feature or `hashbrown` feature for both versions should be added?

Pull request:
Add support for hashbrown v0.15
Hashbrown support for v0.14 is already included and this just extends
the code to cover the new crate version.

Closes #787

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b219003..7bb7f67 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -88,7 +88,7 @@ jobs:
       - name: "Check no_std+alloc (No Default Features / ${{ matrix.os }} / ${{ matrix.rust }})"
         run: cargo check --package serde_with --no-default-features --features=alloc --target thumbv7em-none-eabihf
       - name: "Check no_std+alloc+optional (No Default Features / ${{ matrix.os }} / ${{ matrix.rust }})"
-        run: cargo check --package serde_with --no-default-features --features=alloc,base64,chrono_0_4,hashbrown_0_14,hex,indexmap_1,indexmap_2,json,time_0_3 --target thumbv7em-none-eabihf
+        run: cargo check --package serde_with --no-default-features --features=alloc,base64,chrono_0_4,hashbrown_0_14,hashbrown_0_15,hex,indexmap_1,indexmap_2,json,time_0_3 --target thumbv7em-none-eabihf
 
       # The tests are split into build and run steps, to see the time impact of each
       # cargo test --all-targets does NOT run doctests
diff --git a/Cargo.lock b/Cargo.lock
index 2a43026..e66fe11 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -324,6 +324,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -881,6 +890,7 @@ dependencies = [
  "fnv",
  "glob",
  "hashbrown 0.14.3",
+ "hashbrown 0.15.0",
  "hex",
  "indexmap 1.9.3",
  "indexmap 2.1.0",
diff --git a/serde_with/CHANGELOG.md b/serde_with/CHANGELOG.md
index ed4c452..596b651 100644
--- a/serde_with/CHANGELOG.md
+++ b/serde_with/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 ## [Unreleased]
 
+### Added
+
+* Add support for `hashbrown` v0.15 (#787/#790)
+
+    This extends the existing support for `hashbrown` v0.14 to the newly released version.
+
 ## [3.10.0] - 2024-10-01
 
 ### Added
diff --git a/serde_with/Cargo.toml b/serde_with/Cargo.toml
index 1e0ac82..0006d26 100644
--- a/serde_with/Cargo.toml
+++ b/serde_with/Cargo.toml
@@ -73,6 +73,12 @@ chrono_0_4 = ["dep:chrono_0_4"]
 ## It enables the `alloc` feature.
 ## Some functionality is only available when `std` is enabled too.
 hashbrown_0_14 = ["dep:hashbrown_0_14", "alloc"]
+## The feature enables `hashbrown::{HashMap, HashSet}` as supported containers.
+##
+## This pulls in `hashbrown` v0.15 as a dependency.
+## It enables the `alloc` feature.
+## Some functionality is only available when `std` is enabled too.
+hashbrown_0_15 = ["dep:hashbrown_0_15", "alloc"]
 ## The feature enables serializing data in hex format.
 ##
 ## This pulls in `hex` as a dependency.
@@ -126,6 +132,7 @@ chrono_0_4 = {package = "chrono", version = "0.4.20", optional = true, default-f
 doc-comment = {version = "0.3.3", optional = true}
 document-features = {version = "0.2.7", optional = true}
 hashbrown_0_14 = {package = "hashbrown", version = "0.14.0", optional = true, default-features = false, features = ["serde"]}
+hashbrown_0_15 = {package = "hashbrown", version = "0.15.0", optional = true, default-features = false, features = ["serde"]}
 hex = {version = "0.4.3", optional = true, default-features = false}
 indexmap_1 = {package = "indexmap", version = "1.8", optional = true, default-features = false, features = ["serde-1"]}
 indexmap_2 = {package = "indexmap", version = "2.0", optional = true, default-features = false, features = ["serde"]}
@@ -175,6 +182,11 @@ name = "hashbrown_0_14"
 path = "tests/hashbrown_0_14.rs"
 required-features = ["hashbrown_0_14", "macros"]
 
+[[test]]
+name = "hashbrown_0_15"
+path = "tests/hashbrown_0_15.rs"
+required-features = ["hashbrown_0_15", "macros"]
+
 [[test]]
 name = "indexmap_1"
 path = "tests/indexmap_1.rs"
diff --git a/serde_with/src/de/duplicates.rs b/serde_with/src/de/duplicates.rs
index 3dc7b9d..5a2dde9 100644
--- a/serde_with/src/de/duplicates.rs
+++ b/serde_with/src/de/duplicates.rs
@@ -8,6 +8,8 @@ use crate::{
 };
 #[cfg(feature = "hashbrown_0_14")]
 use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014};
+#[cfg(feature = "hashbrown_0_15")]
+use hashbrown_0_15::{HashMap as HashbrownMap015, HashSet as HashbrownSet015};
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
 #[cfg(feature = "indexmap_2")]
diff --git a/serde_with/src/de/impls.rs b/serde_with/src/de/impls.rs
index 2547420..93b370d 100644
--- a/serde_with/src/de/impls.rs
+++ b/serde_with/src/de/impls.rs
@@ -2,6 +2,8 @@ pub(crate) use self::macros::*;
 use crate::{formats::*, prelude::*};
 #[cfg(feature = "hashbrown_0_14")]
 use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014};
+#[cfg(feature = "hashbrown_0_15")]
+use hashbrown_0_15::{HashMap as HashbrownMap015, HashSet as HashbrownSet015};
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
 #[cfg(feature = "indexmap_2")]
@@ -32,6 +34,11 @@ pub(crate) mod macros {
                 HashbrownMap014<K: Eq + Hash, V, S: BuildHasher + Default>,
                 (|size| HashbrownMap014::with_capacity_and_hasher(size, Default::default()))
             );
+            #[cfg(feature = "hashbrown_0_15")]
+            $m!(
+                HashbrownMap015<K: Eq + Hash, V, S: BuildHasher + Default>,
+                (|size| HashbrownMap015::with_capacity_and_hasher(size, Default::default()))
+            );
             #[cfg(feature = "indexmap_1")]
             $m!(
                 IndexMap<K: Eq + Hash, V, S: BuildHasher + Default>,
@@ -61,6 +68,12 @@ pub(crate) mod macros {
                 (|size| HashbrownSet014::with_capacity_and_hasher(size, S::default())),
                 insert
             );
+            #[cfg(feature = "hashbrown_0_15")]
+            $m!(
+                HashbrownSet015<T: Eq + Hash, S: BuildHasher + Default>,
+                (|size| HashbrownSet015::with_capacity_and_hasher(size, S::default())),
+                insert
+            );
             #[cfg(feature = "indexmap_1")]
             $m!(
                 IndexSet<T: Eq + Hash, S: BuildHasher + Default>,
diff --git a/serde_with/src/de/skip_error.rs b/serde_with/src/de/skip_error.rs
index dd7fa1c..20f48cf 100644
--- a/serde_with/src/de/skip_error.rs
+++ b/serde_with/src/de/skip_error.rs
@@ -2,6 +2,8 @@ use super::impls::macros::foreach_map;
 use crate::prelude::*;
 #[cfg(feature = "hashbrown_0_14")]
 use hashbrown_0_14::HashMap as HashbrownMap014;
+#[cfg(feature = "hashbrown_0_15")]
+use hashbrown_0_15::HashMap as HashbrownMap015;
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::IndexMap;
 #[cfg(feature = "indexmap_2")]
diff --git a/serde_with/src/duplicate_key_impls/error_on_duplicate.rs b/serde_with/src/duplicate_key_impls/error_on_duplicate.rs
index 715c215..2b80346 100644
--- a/serde_with/src/duplicate_key_impls/error_on_duplicate.rs
+++ b/serde_with/src/duplicate_key_impls/error_on_duplicate.rs
@@ -54,6 +54,26 @@ where
     }
 }
 
+#[cfg(feature = "hashbrown_0_15")]
+impl<T, S> PreventDuplicateInsertsSet<T> for hashbrown_0_15::HashSet<T, S>
+where
+    T: Eq + Hash,
+    S: BuildHasher + Default,
+{
+    #[inline]
+    fn new(size_hint: Option<usize>) -> Self {
+        match size_hint {
+            Some(size) => Self::with_capacity_and_hasher(size, S::default()),
+            None => Self::with_hasher(S::default()),
+        }
+    }
+
+    #[inline]
+    fn insert(&mut self, value: T) -> bool {
+        self.insert(value)
+    }
+}
+
 #[cfg(feature = "indexmap_1")]
 impl<T, S> PreventDuplicateInsertsSet<T> for indexmap_1::IndexSet<T, S>
 where
@@ -149,6 +169,26 @@ where
     }
 }
 
+#[cfg(feature = "hashbrown_0_15")]
+impl<K, V, S> PreventDuplicateInsertsMap<K, V> for hashbrown_0_15::HashMap<K, V, S>
+where
+    K: Eq + Hash,
+    S: BuildHasher + Default,
+{
+    #[inline]
+    fn new(size_hint: Option<usize>) -> Self {
+        match size_hint {
+            Some(size) => Self::with_capacity_and_hasher(size, S::default()),
+            None => Self::with_hasher(S::default()),
+        }
+    }
+
+    #[inline]
+    fn insert(&mut self, key: K, value: V) -> bool {
+        self.insert(key, value).is_none()
+    }
+}
+
 #[cfg(feature = "indexmap_1")]
 impl<K, V, S> PreventDuplicateInsertsMap<K, V> for indexmap_1::IndexMap<K, V, S>
 where
diff --git a/serde_with/src/duplicate_key_impls/first_value_wins.rs b/serde_with/src/duplicate_key_impls/first_value_wins.rs
index e2413e6..38f4bff 100644
--- a/serde_with/src/duplicate_key_impls/first_value_wins.rs
+++ b/serde_with/src/duplicate_key_impls/first_value_wins.rs
@@ -63,6 +63,34 @@ where
     }
 }
 
+#[cfg(feature = "hashbrown_0_15")]
+impl<K, V, S> DuplicateInsertsFirstWinsMap<K, V> for hashbrown_0_15::HashMap<K, V, S>
+where
+    K: Eq + Hash,
+    S: BuildHasher + Default,
+{
+    #[inline]
+    fn new(size_hint: Option<usize>) -> Self {
+        match size_hint {
+            Some(size) => Self::with_capacity_and_hasher(size, S::default()),
+            None => Self::with_hasher(S::default()),
+        }
+    }
+
+    #[inline]
+    fn insert(&mut self, key: K, value: V) {
+        use hashbrown_0_15::hash_map::Entry;
+
+        match self.entry(key) {
+            // we want to keep the first value, so do nothing
+            Entry::Occupied(_) => {}
+            Entry::Vacant(vacant) => {
+                vacant.insert(value);
+            }
+        }
+    }
+}
+
 #[cfg(feature = "indexmap_1")]
 impl<K, V, S> DuplicateInsertsFirstWinsMap<K, V> for indexmap_1::IndexMap<K, V, S>
 where
diff --git a/serde_with/src/duplicate_key_impls/last_value_wins.rs b/serde_with/src/duplicate_key_impls/last_value_wins.rs
index 970197a..af75f49 100644
--- a/serde_with/src/duplicate_key_impls/last_value_wins.rs
+++ b/serde_with/src/duplicate_key_impls/last_value_wins.rs
@@ -49,6 +49,27 @@ where
     }
 }
 
+#[cfg(feature = "hashbrown_0_15")]
+impl<T, S> DuplicateInsertsLastWinsSet<T> for hashbrown_0_15::HashSet<T, S>
+where
+    T: Eq + Hash,
+    S: BuildHasher + Default,
+{
+    #[inline]
+    fn new(size_hint: Option<usize>) -> Self {
+        match size_hint {
+            Some(size) => Self::with_capacity_and_hasher(size, S::default()),
+            None => Self::with_hasher(S::default()),
+        }
+    }
+
+    #[inline]
+    fn replace(&mut self, value: T) {
+        // Hashset already fulfils the contract
+        self.replace(value);
+    }
+}
+
 #[cfg(feature = "indexmap_1")]
 impl<T, S> DuplicateInsertsLastWinsSet<T> for indexmap_1::IndexSet<T, S>
 where
diff --git a/serde_with/src/schemars_0_8.rs b/serde_with/src/schemars_0_8.rs
index 603bffb..0864c4e 100644
--- a/serde_with/src/schemars_0_8.rs
+++ b/serde_with/src/schemars_0_8.rs
@@ -787,6 +787,8 @@ map_first_last_wins_schema!(BTreeMap<K, V>);
 map_first_last_wins_schema!(=> S HashMap<K, V, S>);
 #[cfg(feature = "hashbrown_0_14")]
 map_first_last_wins_schema!(=> S hashbrown_0_14::HashMap<K, V, S>);
+#[cfg(feature = "hashbrown_0_15")]
+map_first_last_wins_schema!(=> S hashbrown_0_15::HashMap<K, V, S>);
 #[cfg(feature = "indexmap_1")]
 map_first_last_wins_schema!(=> S indexmap_1::IndexMap<K, V, S>);
 #[cfg(feature = "indexmap_2")]
diff --git a/serde_with/src/ser/duplicates.rs b/serde_with/src/ser/duplicates.rs
index 8c83783..dc89b49 100644
--- a/serde_with/src/ser/duplicates.rs
+++ b/serde_with/src/ser/duplicates.rs
@@ -2,6 +2,8 @@ use super::impls::macros::{foreach_map, foreach_set};
 use crate::prelude::*;
 #[cfg(feature = "hashbrown_0_14")]
 use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014};
+#[cfg(feature = "hashbrown_0_15")]
+use hashbrown_0_15::{HashMap as HashbrownMap015, HashSet as HashbrownSet015};
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
 #[cfg(feature = "indexmap_2")]
diff --git a/serde_with/src/ser/impls.rs b/serde_with/src/ser/impls.rs
index 03ae0b3..7fc3290 100644
--- a/serde_with/src/ser/impls.rs
+++ b/serde_with/src/ser/impls.rs
@@ -2,6 +2,8 @@ pub(crate) use self::macros::*;
 use crate::{formats::Strictness, prelude::*};
 #[cfg(feature = "hashbrown_0_14")]
 use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014};
+#[cfg(feature = "hashbrown_0_15")]
+use hashbrown_0_15::{HashMap as HashbrownMap015, HashSet as HashbrownSet015};
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
 #[cfg(feature = "indexmap_2")]
@@ -29,6 +31,8 @@ pub(crate) mod macros {
         $m!(HashMap<K, V, H: Sized>);
         #[cfg(feature = "hashbrown_0_14")]
         $m!(HashbrownMap014<K, V, H: Sized>);
+        #[cfg(feature = "hashbrown_0_15")]
+        $m!(HashbrownMap015<K, V, H: Sized>);
         #[cfg(feature = "indexmap_1")]
         $m!(IndexMap<K, V, H: Sized>);
         #[cfg(feature = "indexmap_2")]
@@ -44,6 +48,8 @@ pub(crate) mod macros {
         $m!(HashSet<$T, H: Sized>);
         #[cfg(feature = "hashbrown_0_14")]
         $m!(HashbrownSet014<$T, H: Sized>);
+        #[cfg(feature = "hashbrown_0_15")]
+        $m!(HashbrownSet015<$T, H: Sized>);
         #[cfg(feature = "indexmap_1")]
         $m!(IndexSet<$T, H: Sized>);
         #[cfg(feature = "indexmap_2")]
diff --git a/serde_with/src/ser/skip_error.rs b/serde_with/src/ser/skip_error.rs
index 07c8c99..0eea6e0 100644
--- a/serde_with/src/ser/skip_error.rs
+++ b/serde_with/src/ser/skip_error.rs
@@ -2,6 +2,8 @@ use super::impls::macros::foreach_map;
 use crate::prelude::*;
 #[cfg(feature = "hashbrown_0_14")]
 use hashbrown_0_14::HashMap as HashbrownMap014;
+#[cfg(feature = "hashbrown_0_15")]
+use hashbrown_0_15::HashMap as HashbrownMap015;
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::IndexMap;
 #[cfg(feature = "indexmap_2")]


Test patch:
diff --git a/serde_with/tests/hashbrown_0_15.rs b/serde_with/tests/hashbrown_0_15.rs
new file mode 100644
index 0000000..4352d58
--- /dev/null
+++ b/serde_with/tests/hashbrown_0_15.rs
@@ -0,0 +1,388 @@
+//! Test Cases
+
+mod utils;
+
+use crate::utils::{check_deserialization, check_error_deserialization, is_equal};
+use expect_test::expect;
+use serde::{Deserialize, Serialize};
+use serde_with::{serde_as, DisplayFromStr, Same};
+use std::net::IpAddr;
+
+type HashMap<K, V, S = fnv::FnvBuildHasher> = hashbrown_0_15::HashMap<K, V, S>;
+type HashSet<V, S = fnv::FnvBuildHasher> = hashbrown_0_15::HashSet<V, S>;
+
+#[test]
+fn test_hashmap() {
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct S(#[serde_as(as = "HashMap<DisplayFromStr, DisplayFromStr>")] HashMap<u8, u32>);
+
+    // Normal
+    is_equal(
+        S([(1, 1), (3, 3), (111, 111)].iter().copied().collect()),
+        expect![[r#"
+          {
+            "1": "1",
+            "3": "3",
+            "111": "111"
+          }"#]],
+    );
+    is_equal(S(HashMap::default()), expect![[r#"{}"#]]);
+
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct SStd(
+        #[serde_as(
+            as = "HashMap<DisplayFromStr, DisplayFromStr, core::hash::BuildHasherDefault<std::collections::hash_map::DefaultHasher>>"
+        )]
+        HashMap<u8, u32, core::hash::BuildHasherDefault<std::collections::hash_map::DefaultHasher>>,
+    );
+
+    // Normal
+    is_equal(
+        SStd([(1, 1)].iter().copied().collect()),
+        expect![[r#"
+          {
+            "1": "1"
+          }"#]],
+    );
+    is_equal(SStd(HashMap::default()), expect![[r#"{}"#]]);
+}
+
+#[test]
+fn test_hashset() {
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct S(#[serde_as(as = "HashSet<DisplayFromStr>")] HashSet<u32>);
+
+    // Normal
+    is_equal(
+        S([1, 2, 3, 4, 5].iter().copied().collect()),
+        expect![[r#"
+          [
+            "5",
+            "4",
+            "1",
+            "3",
+            "2"
+          ]"#]],
+    );
+    is_equal(S(HashSet::default()), expect![[r#"[]"#]]);
+
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct SStd(
+        #[serde_as(
+            as = "HashSet<DisplayFromStr, core::hash::BuildHasherDefault<std::collections::hash_map::DefaultHasher>>"
+        )]
+        HashSet<u32, core::hash::BuildHasherDefault<std::collections::hash_map::DefaultHasher>>,
+    );
+
+    // Normal
+    is_equal(
+        SStd([1].iter().copied().collect()),
+        expect![[r#"
+          [
+            "1"
+          ]"#]],
+    );
+    is_equal(SStd(HashSet::default()), expect![[r#"[]"#]]);
+}
+
+#[test]
+fn test_map_as_tuple_list() {
+    let ip = "1.2.3.4".parse().unwrap();
+    let ip2 = "255.255.255.255".parse().unwrap();
+
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct SI(#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")] HashMap<u32, IpAddr>);
+
+    let map: HashMap<_, _> = vec![(1, ip), (10, ip), (200, ip2)].into_iter().collect();
+    is_equal(
+        SI(map.clone()),
+        expect![[r#"
+            [
+              [
+                "1",
+                "1.2.3.4"
+              ],
+              [
+                "200",
+                "255.255.255.255"
+              ],
+              [
+                "10",
+                "1.2.3.4"
+              ]
+            ]"#]],
+    );
+
+    #[serde_as]
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct SI2(#[serde_as(as = "Vec<(Same, DisplayFromStr)>")] HashMap<u32, IpAddr>);
+
+    is_equal(
+        SI2(map),
+        expect![[r#"
+            [
+              [
+                1,
+                "1.2.3.4"
+              ],
+              [
+                200,
+                "255.255.255.255"
+              ],
+              [
+                10,
+                "1.2.3.4"
+              ]
+            ]"#]],
+    );
+}
+
+#[test]
+fn duplicate_key_first_wins_hashmap() {
+    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+    struct S(#[serde(with = "::serde_with::rust::maps_first_key_wins")] HashMap<usize, usize>);
+
+    // Different value and key always works
+    is_equal(
+        S(HashMap::from_iter(vec![(1, 1), (2, 2), (3, 3)])),
+        expect![[r#"
+            {
+              "1": 1,
+              "3": 3,
+              "2": 2
+            }"#]],
+    );
+
+    // Same value for different keys is ok
+    is_equal(
+        S(HashMap::from_iter(vec![(1, 1), (2, 1), (3, 1)])),
+        expect![[r#"
+            {
+              "1": 1,
+              "3": 1,
+              "2": 1
+            }"#]],
+    );
+
+    // Duplicate keys, the first one is used
+    check_deserialization(
+        S(HashMap::from_iter(vec![(1, 1), (2, 2)])),
+        r#"{"1": 1, "2": 2, "1": 3}"#,
+    );
+}
+
+#[test]
+fn prohibit_duplicate_key_hashmap() {
+    #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
+    struct S(
+        #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")] HashMap<usize, usize>,
+    );
+
+    // Different value and key always works
+    is_equal(
+        S(HashMap::from_iter(vec![(1, 1), (2, 2), (3, 3)])),
+        expect![[r#"
+            {
+              "1": 1,
+              "3": 3,
+              "2": 2
+            }"#]],
+    );
+
+    // Same value for different keys is ok
+    is_equal(
+        S(HashMap::from_iter(vec![(1, 1), (2, 1), (3, 1)])),
+        expect![[r#"
+            {
+              "1": 1,
+              "3": 1,
+              "2": 1
+            }"#]],
+    );
+
+    // Duplicate keys are an error
+    check_error_deserialization::<S>(
+        r#"{"1": 1, "2": 2, "1": 3}"#,
+        expect![[r#"invalid entry: found duplicate key at line 1 column 24"#]],
+    );
+}
+
+#[test]
+fn duplicate_value_last_wins_hashset() {
+    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+    struct S(#[serde(with = "::serde_with::rust::sets_last_value_wins")] HashSet<W>);
+
+    #[derive(Debug, Eq, Deserialize, Serialize)]
+    struct W(i32, bool);
+    impl PartialEq for W {
+        fn eq(&self, other: &Self) -> bool {
+            self.0 == other.0
+        }
+    }
+    impl std::hash::Hash for W {
+        fn hash<H>(&self, state: &mut H)
+        where
+            H: std::hash::Hasher,
+        {
+            self.0.hash(state);
+        }
+    }
+
+    // Different values always work
+    is_equal(
+        S(HashSet::from_iter(vec![
+            W(1, true),
+            W(2, false),
+            W(3, true),
+        ])),
+        expect![[r#"
+            [
+              [
+                1,
+                true
+              ],
+              [
+                3,
+                true
+              ],
+              [
+                2,
+                false
+              ]
+            ]"#]],
+    );
+
+    let value: S = serde_json::from_str(
+        r#"[
+        [1, false],
+        [1, true],
+        [2, true],
+        [2, false]
+    ]"#,
+    )
+    .unwrap();
+    let entries: Vec<_> = value.0.into_iter().collect();
+    assert_eq!(1, entries[0].0);
+    assert!(entries[0].1);
+    assert_eq!(2, entries[1].0);
+    assert!(!entries[1].1);
+}
+
+#[test]
+fn prohibit_duplicate_value_hashset() {
+    #[derive(Debug, PartialEq, Deserialize, Serialize)]
+    struct S(#[serde(with = "::serde_with::rust::sets_duplicate_value_is_error")] HashSet<usize>);
+
+    is_equal(
+        S(HashSet::from_iter(vec![1, 2, 3, 4])),
+        expect![[r#"
+            [
+              4,
+              1,
+              3,
+              2
+            ]"#]],
+    );
+    check_error_deserialization::<S>(
+        r#"[1, 2, 3, 4, 1]"#,
+        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
+            }"#]],
+    );
+}