Repository: jonasbb/serde_with
Pull request #637 (2023-08-14 21:33:52)

feat: add optional support for hasbrown Map & Set
This PR is not yet ready to merge as I've never setup a `#[no_std]` friendly crate before. Looking for guidance for how to properly setup the Cargo.toml to support this (and perhaps validate this holds true).

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jonasbb/serde_with/pull/637?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) Report
> Merging [#637](https://app.codecov.io/gh/jonasbb/serde_with/pull/637?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) (8b0ec82) into [master](https://app.codecov.io/gh/jonasbb/serde_with/commit/fc30b63125c501b12e1c9e53e50a7618c4180b47?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) (fc30b63) will **increase** coverage by `0.24%`.
> The diff coverage is `82.75%`.

```diff
@@            Coverage Diff             @@
##           master     #637      +/-   ##
==========================================
+ Coverage   64.44%   64.68%   +0.24%     
==========================================
  Files          36       36              
  Lines        2157     2186      +29     
==========================================
+ Hits         1390     1414      +24     
- Misses        767      772       +5     
```


| [Files Changed](https://app.codecov.io/gh/jonasbb/serde_with/pull/637?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart) | Coverage Δ | |
|---|---|---|
| [serde\_with/src/de/duplicates.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/637?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC9zcmMvZGUvZHVwbGljYXRlcy5ycw==) | `0.00% <ø> (ø)` | |
| [serde\_with/src/ser/impls.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/637?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC9zcmMvc2VyL2ltcGxzLnJz) | `91.66% <ø> (ø)` | |
| [...e\_with/src/duplicate\_key\_impls/first\_value\_wins.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/637?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC9zcmMvZHVwbGljYXRlX2tleV9pbXBscy9maXJzdF92YWx1ZV93aW5zLnJz) | `79.06% <77.77%> (-0.35%)` | :arrow_down: |
| [...with/src/duplicate\_key\_impls/error\_on\_duplicate.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/637?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC9zcmMvZHVwbGljYXRlX2tleV9pbXBscy9lcnJvcl9vbl9kdXBsaWNhdGUucnM=) | `85.71% <83.33%> (-0.65%)` | :arrow_down: |
| [...de\_with/src/duplicate\_key\_impls/last\_value\_wins.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/637?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC9zcmMvZHVwbGljYXRlX2tleV9pbXBscy9sYXN0X3ZhbHVlX3dpbnMucnM=) | `85.71% <83.33%> (-0.65%)` | :arrow_down: |
| [serde\_with/src/de/impls.rs](https://app.codecov.io/gh/jonasbb/serde_with/pull/637?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart#diff-c2VyZGVfd2l0aC9zcmMvZGUvaW1wbHMucnM=) | `59.00% <100.00%> (+0.16%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jonas+Bushart)


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index c28559e..1fc8660 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -89,7 +89,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,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,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 894d43d..bf4b3e8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -262,6 +262,9 @@ name = "hashbrown"
 version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "hex"
@@ -607,6 +610,7 @@ dependencies = [
  "expect-test",
  "fnv",
  "glob",
+ "hashbrown 0.14.0",
  "hex",
  "indexmap 1.9.2",
  "indexmap 2.0.0",
diff --git a/serde_with/Cargo.toml b/serde_with/Cargo.toml
index 989bd21..6b2df0f 100644
--- a/serde_with/Cargo.toml
+++ b/serde_with/Cargo.toml
@@ -54,6 +54,7 @@ chrono = ["chrono_0_4"]
 chrono_0_4 = ["dep:chrono_0_4"]
 guide = ["dep:doc-comment", "macros", "std"]
 hex = ["dep:hex", "alloc"]
+hashbrown_0_14 = ["dep:hashbrown_0_14", "alloc"]
 indexmap = ["indexmap_1"]
 indexmap_1 = ["dep:indexmap_1", "alloc"]
 indexmap_2 = ["dep:indexmap_2", "alloc"]
@@ -66,6 +67,7 @@ time_0_3 = ["dep:time_0_3"]
 base64 = {version = "0.21.0", optional = true, default-features = false}
 chrono_0_4 = {package = "chrono", version = "0.4.20", optional = true, default-features = false, features = ["serde"]}
 doc-comment = {version = "0.3.3", optional = true}
+hashbrown_0_14 = {package = "hashbrown", version = "0.14.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"]}
@@ -112,6 +114,11 @@ name = "hex"
 path = "tests/hex.rs"
 required-features = ["hex", "macros"]
 
+[[test]]
+name = "hashbrown_0_14"
+path = "tests/hashbrown_0_14.rs"
+required-features = ["hashbrown_0_14", "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 246b997..c896d7a 100644
--- a/serde_with/src/de/duplicates.rs
+++ b/serde_with/src/de/duplicates.rs
@@ -1,4 +1,4 @@
-use super::impls::{foreach_map_create, foreach_set_create};
+use super::impls::{foreach_map, foreach_set};
 use crate::{
     duplicate_key_impls::{
         DuplicateInsertsFirstWinsMap, DuplicateInsertsLastWinsSet, PreventDuplicateInsertsMap,
@@ -7,6 +7,8 @@ use crate::{
     prelude::*,
     MapFirstKeyWins, MapPreventDuplicates, SetLastValueWins, SetPreventDuplicates,
 };
+#[cfg(feature = "hashbrown_0_14")]
+use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014};
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
 #[cfg(feature = "indexmap_2")]
@@ -73,14 +75,14 @@ where
 #[cfg(feature = "alloc")]
 macro_rules! set_impl {
     (
-        $ty:ident < T $(: $tbound1:ident $(+ $tbound2:ident)*)* $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)* )* >,
+        $ty:ident < T $(: $tbound1:ident $(+ $tbound2:ident)*)? $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)* )* >,
         $with_capacity:expr,
         $append:ident
     ) => {
         impl<'de, T, TAs $(, $typaram)*> DeserializeAs<'de, $ty<T $(, $typaram)*>> for SetPreventDuplicates<TAs>
         where
             TAs: DeserializeAs<'de, T>,
-            $(T: $tbound1 $(+ $tbound2)*,)*
+            $(T: $tbound1 $(+ $tbound2)*,)?
             $($typaram: $bound1 $(+ $bound2)*),*
         {
             fn deserialize_as<D>(deserializer: D) -> Result<$ty<T $(, $typaram)*>, D::Error>
@@ -96,7 +98,7 @@ macro_rules! set_impl {
         impl<'de, T, TAs $(, $typaram)*> DeserializeAs<'de, $ty<T $(, $typaram)*>> for SetLastValueWins<TAs>
         where
             TAs: DeserializeAs<'de, T>,
-            $(T: $tbound1 $(+ $tbound2)*,)*
+            $(T: $tbound1 $(+ $tbound2)*,)?
             $($typaram: $bound1 $(+ $bound2)*),*
         {
             fn deserialize_as<D>(deserializer: D) -> Result<$ty<T $(, $typaram)*>, D::Error>
@@ -109,7 +111,7 @@ macro_rules! set_impl {
         }
     }
 }
-foreach_set_create!(set_impl);
+foreach_set!(set_impl);
 
 struct MapPreventDuplicatesVisitor<MAP, K, KAs, V, VAs>(PhantomData<(MAP, K, KAs, V, VAs)>);
 
@@ -178,7 +180,7 @@ where
 #[cfg(feature = "alloc")]
 macro_rules! map_impl {
     (
-        $ty:ident < K $(: $kbound1:ident $(+ $kbound2:ident)*)*, V $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)*)* >,
+        $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)*>>
@@ -186,7 +188,7 @@ macro_rules! map_impl {
         where
             KAs: DeserializeAs<'de, K>,
             VAs: DeserializeAs<'de, V>,
-            $(K: $kbound1 $(+ $kbound2)*,)*
+            $(K: $kbound1 $(+ $kbound2)*,)?
             $($typaram: $bound1 $(+ $bound2)*),*
         {
             fn deserialize_as<D>(deserializer: D) -> Result<$ty<K, V $(, $typaram)*>, D::Error>
@@ -208,7 +210,7 @@ macro_rules! map_impl {
         where
             KAs: DeserializeAs<'de, K>,
             VAs: DeserializeAs<'de, V>,
-            $(K: $kbound1 $(+ $kbound2)*,)*
+            $(K: $kbound1 $(+ $kbound2)*,)?
             $($typaram: $bound1 $(+ $bound2)*),*
         {
             fn deserialize_as<D>(deserializer: D) -> Result<$ty<K, V $(, $typaram)*>, D::Error>
@@ -222,4 +224,4 @@ macro_rules! map_impl {
         }
     };
 }
-foreach_map_create!(map_impl);
+foreach_map!(map_impl);
diff --git a/serde_with/src/de/impls.rs b/serde_with/src/de/impls.rs
index c5f9eb7..78357fd 100644
--- a/serde_with/src/de/impls.rs
+++ b/serde_with/src/de/impls.rs
@@ -1,4 +1,6 @@
 use crate::{formats::*, prelude::*};
+#[cfg(feature = "hashbrown_0_14")]
+use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014};
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
 #[cfg(feature = "indexmap_2")]
@@ -11,20 +13,6 @@ use indexmap_2::{IndexMap as IndexMap2, IndexSet as IndexSet2};
 type BoxedSlice<T> = Box<[T]>;
 
 macro_rules! foreach_map {
-    ($m:ident) => {
-        #[cfg(feature = "alloc")]
-        $m!(BTreeMap<K: Ord, V>);
-        #[cfg(feature = "std")]
-        $m!(HashMap<K: Eq + Hash, V, H: Sized>);
-        #[cfg(all(feature = "std", feature = "indexmap_1"))]
-        $m!(IndexMap<K: Eq + Hash, V, H: Sized>);
-        #[cfg(all(feature = "std", feature = "indexmap_2"))]
-        $m!(IndexMap2<K: Eq + Hash, V, H: Sized>);
-    };
-}
-pub(crate) use foreach_map;
-
-macro_rules! foreach_map_create {
     ($m:ident) => {
         #[cfg(feature = "alloc")]
         $m!(BTreeMap<K: Ord, V>, (|_size| BTreeMap::new()));
@@ -33,6 +21,11 @@ macro_rules! foreach_map_create {
             HashMap<K: Eq + Hash, V, S: BuildHasher + Default>,
             (|size| HashMap::with_capacity_and_hasher(size, Default::default()))
         );
+        #[cfg(feature = "hashbrown_0_14")]
+        $m!(
+            HashbrownMap014<K: Eq + Hash, V, S: BuildHasher + Default>,
+            (|size| HashbrownMap014::with_capacity_and_hasher(size, Default::default()))
+        );
         #[cfg(feature = "indexmap_1")]
         $m!(
             IndexMap<K: Eq + Hash, V, S: BuildHasher + Default>,
@@ -45,23 +38,9 @@ macro_rules! foreach_map_create {
         );
     };
 }
-pub(crate) use foreach_map_create;
+pub(crate) use foreach_map;
 
 macro_rules! foreach_set {
-    ($m:ident) => {
-        #[cfg(feature = "alloc")]
-        $m!(BTreeSet<(K, V): Ord>);
-        #[cfg(feature = "std")]
-        $m!(HashSet<(K, V): Eq + Hash>);
-        #[cfg(all(feature = "std", feature = "indexmap_1"))]
-        $m!(IndexSet<(K, V): Eq + Hash>);
-        #[cfg(all(feature = "std", feature = "indexmap_2"))]
-        $m!(IndexSet2<(K, V): Eq + Hash>);
-    }
-}
-pub(crate) use foreach_set;
-
-macro_rules! foreach_set_create {
     ($m:ident) => {
         #[cfg(feature = "alloc")]
         $m!(BTreeSet<T: Ord>, (|_| BTreeSet::new()), insert);
@@ -71,6 +50,12 @@ macro_rules! foreach_set_create {
             (|size| HashSet::with_capacity_and_hasher(size, S::default())),
             insert
         );
+        #[cfg(feature = "hashbrown_0_14")]
+        $m!(
+            HashbrownSet014<T: Eq + Hash, S: BuildHasher + Default>,
+            (|size| HashbrownSet014::with_capacity_and_hasher(size, S::default())),
+            insert
+        );
         #[cfg(feature = "indexmap_1")]
         $m!(
             IndexSet<T: Eq + Hash, S: BuildHasher + Default>,
@@ -85,28 +70,12 @@ macro_rules! foreach_set_create {
         );
     };
 }
-pub(crate) use foreach_set_create;
+pub(crate) use foreach_set;
 
 macro_rules! foreach_seq {
     ($m:ident) => {
         foreach_set!($m);
 
-        #[cfg(feature = "alloc")]
-        $m!(BinaryHeap<(K, V): Ord>);
-        #[cfg(feature = "alloc")]
-        $m!(LinkedList<(K, V)>);
-        #[cfg(feature = "alloc")]
-        $m!(Vec<(K, V)>);
-        #[cfg(feature = "alloc")]
-        $m!(VecDeque<(K, V)>);
-    }
-}
-pub(crate) use foreach_seq;
-
-macro_rules! foreach_seq_create {
-    ($m:ident) => {
-        foreach_set_create!($m);
-
         #[cfg(feature = "alloc")]
         $m!(
             BinaryHeap<T: Ord>,
@@ -127,7 +96,7 @@ macro_rules! foreach_seq_create {
         );
     };
 }
-pub(crate) use foreach_seq_create;
+pub(crate) use foreach_seq;
 
 ///////////////////////////////////////////////////////////////////////////////
 // region: Simple Wrapper types (e.g., Box, Option)
@@ -378,7 +347,7 @@ where
 #[cfg(feature = "alloc")]
 macro_rules! seq_impl {
     (
-        $ty:ident < T $(: $tbound1:ident $(+ $tbound2:ident)*)* $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)* )* >,
+        $ty:ident < T $(: $tbound1:ident $(+ $tbound2:ident)*)? $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)* )* >,
         $with_capacity:expr,
         $append:ident
     ) => {
@@ -389,7 +358,7 @@ macro_rules! seq_impl {
         impl<'de, T, U $(, $typaram)*> DeserializeAs<'de, $ty<T $(, $typaram)*>> for $ty<U $(, $typaram)*>
         where
             U: DeserializeAs<'de, T>,
-            $(T: $tbound1 $(+ $tbound2)*,)*
+            $(T: $tbound1 $(+ $tbound2)*,)?
             $($typaram: $bound1 $(+ $bound2)*),*
         {
             fn deserialize_as<D>(deserializer: D) -> Result<$ty<T $(, $typaram)*>, D::Error>
@@ -403,7 +372,7 @@ macro_rules! seq_impl {
                 impl<'de, T, U $(, $typaram)*> Visitor<'de> for SeqVisitor<T, U $(, $typaram)*>
                 where
                     U: DeserializeAs<'de, T>,
-                    $(T: $tbound1 $(+ $tbound2)*,)*
+                    $(T: $tbound1 $(+ $tbound2)*,)?
                     $($typaram: $bound1 $(+ $bound2)*),*
                 {
                     type Value = $ty<T $(, $typaram)*>;
@@ -438,7 +407,7 @@ macro_rules! seq_impl {
         }
     };
 }
-foreach_seq_create!(seq_impl);
+foreach_seq!(seq_impl);
 
 #[cfg(feature = "alloc")]
 macro_rules! map_impl {
@@ -498,7 +467,7 @@ macro_rules! map_impl {
         }
     }
 }
-foreach_map_create!(map_impl);
+foreach_map!(map_impl);
 
 macro_rules! tuple_impl {
     ($len:literal $($n:tt $t:ident $tas:ident)+) => {
@@ -567,28 +536,34 @@ tuple_impl!(16 0 T0 As0 1 T1 As1 2 T2 As2 3 T3 As3 4 T4 As4 5 T5 As5 6 T6 As6 7
 
 #[cfg(feature = "alloc")]
 macro_rules! map_as_tuple_seq_intern {
-    ($tyorig:ident < K $(: $kbound1:ident $(+ $kbound2:ident)*)*, V $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)*)* >, $ty:ident <(KAs, VAs)>) => {
-        impl<'de, K, KAs, V, VAs> DeserializeAs<'de, $tyorig<K, V>> for $ty<(KAs, VAs)>
+    (
+        $tyorig:ident < K $(: $kbound1:ident $(+ $kbound2:ident)*)?, V $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)*)* >,
+        $with_capacity:expr,
+        $ty:ident <(KAs, VAs)>
+    ) => {
+        impl<'de, K, KAs, V, VAs $(, $typaram)*> DeserializeAs<'de, $tyorig<K, V $(, $typaram)*>> for $ty<(KAs, VAs)>
         where
             KAs: DeserializeAs<'de, K>,
             VAs: DeserializeAs<'de, V>,
-            $(K: $kbound1 $(+ $kbound2)*,)*
+            $(K: $kbound1 $(+ $kbound2)*,)?
+            $($typaram: $bound1 $(+ $bound2)*,)*
         {
-            fn deserialize_as<D>(deserializer: D) -> Result<$tyorig<K, V>, D::Error>
+            fn deserialize_as<D>(deserializer: D) -> Result<$tyorig<K, V $(, $typaram)*>, D::Error>
             where
                 D: Deserializer<'de>,
             {
-                struct SeqVisitor<K, KAs, V, VAs> {
-                    marker: PhantomData<(K, KAs, V, VAs)>,
+                struct SeqVisitor<K, KAs, V, VAs $(, $typaram)*> {
+                    marker: PhantomData<(K, KAs, V, VAs $(, $typaram)*)>,
                 }
 
-                impl<'de, K, KAs, V, VAs> Visitor<'de> for SeqVisitor<K, KAs, V, VAs>
+                impl<'de, K, KAs, V, VAs $(, $typaram)*> Visitor<'de> for SeqVisitor<K, KAs, V, VAs $(, $typaram)*>
                 where
                     KAs: DeserializeAs<'de, K>,
                     VAs: DeserializeAs<'de, V>,
-                    $(K: $kbound1 $(+ $kbound2)*,)*
+                    $(K: $kbound1 $(+ $kbound2)*,)?
+                    $($typaram: $bound1 $(+ $bound2)*,)*
                 {
-                    type Value = $tyorig<K, V>;
+                    type Value = $tyorig<K, V $(, $typaram)*>;
 
                     fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                         formatter.write_str("a sequence")
@@ -611,7 +586,7 @@ macro_rules! map_as_tuple_seq_intern {
                     }
                 }
 
-                let visitor = SeqVisitor::<K, KAs, V, VAs> {
+                let visitor = SeqVisitor::<K, KAs, V, VAs $(, $typaram)*> {
                     marker: PhantomData,
                 };
                 deserializer.deserialize_seq(visitor)
@@ -621,39 +596,49 @@ macro_rules! map_as_tuple_seq_intern {
 }
 #[cfg(feature = "alloc")]
 macro_rules! map_as_tuple_seq {
-    ($tyorig:ident < K $(: $kbound1:ident $(+ $kbound2:ident)*)*, V $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)*)* >) => {
-        map_as_tuple_seq_intern!($tyorig < K $(: $kbound1 $(+ $kbound2)*)* , V $(, $typaram : $bound1 $(+ $bound2)*)* > , Seq<(KAs, VAs)>);
+    (
+        $tyorig:ident < K $(: $kbound1:ident $(+ $kbound2:ident)*)?, V $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)*)* >,
+        $with_capacity:expr
+    ) => {
+        map_as_tuple_seq_intern!($tyorig < K $(: $kbound1 $(+ $kbound2)*)? , V $(, $typaram : $bound1 $(+ $bound2)*)* >, $with_capacity, Seq<(KAs, VAs)>);
         #[cfg(feature = "alloc")]
-        map_as_tuple_seq_intern!($tyorig < K $(: $kbound1 $(+ $kbound2)*)* , V $(, $typaram : $bound1 $(+ $bound2)*)* >, Vec<(KAs, VAs)>);
+        map_as_tuple_seq_intern!($tyorig < K $(: $kbound1 $(+ $kbound2)*)? , V $(, $typaram : $bound1 $(+ $bound2)*)* >, $with_capacity, Vec<(KAs, VAs)>);
     }
 }
 foreach_map!(map_as_tuple_seq);
 
 #[cfg(feature = "alloc")]
 macro_rules! tuple_seq_as_map_impl_intern {
-    ($tyorig:ident < (K, V) $(: $($bound:ident $(+)?)+)?>, $ty:ident <KAs, VAs>) => {
+    (
+        $tyorig:ident < (K, V) $(: $($bound:ident $(+)?)+)? $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)* )* >,
+        $with_capacity:expr,
+        $append:ident,
+        $ty:ident <KAs, VAs>
+    ) => {
         #[allow(clippy::implicit_hasher)]
-        impl<'de, K, KAs, V, VAs> DeserializeAs<'de, $tyorig < (K, V) >> for $ty<KAs, VAs>
+        impl<'de, K, KAs, V, VAs $(, $typaram)*> DeserializeAs<'de, $tyorig < (K, V) $(, $typaram)* >> for $ty<KAs, VAs>
         where
             KAs: DeserializeAs<'de, K>,
             VAs: DeserializeAs<'de, V>,
-            (K, V): $($($bound +)*)*,
+            (K, V): $($($bound +)*)?,
+            $($typaram: $bound1 $(+ $bound2)*,)*
         {
-            fn deserialize_as<D>(deserializer: D) -> Result<$tyorig < (K, V) >, D::Error>
+            fn deserialize_as<D>(deserializer: D) -> Result<$tyorig < (K, V) $(, $typaram)* >, D::Error>
             where
                 D: Deserializer<'de>,
             {
-                struct MapVisitor<K, KAs, V, VAs> {
-                    marker: PhantomData<(K, KAs, V, VAs)>,
+                struct MapVisitor<K, KAs, V, VAs $(, $typaram)*> {
+                    marker: PhantomData<(K, KAs, V, VAs $(, $typaram)*)>,
                 }
 
-                impl<'de, K, KAs, V, VAs> Visitor<'de> for MapVisitor<K, KAs, V, VAs>
+                impl<'de, K, KAs, V, VAs $(, $typaram)*> Visitor<'de> for MapVisitor<K, KAs, V, VAs $(, $typaram)*>
                 where
                     KAs: DeserializeAs<'de, K>,
                     VAs: DeserializeAs<'de, V>,
-                    (K, V): $($($bound +)*)*,
+                    (K, V): $($($bound +)*)?,
+                    $($typaram: $bound1 $(+ $bound2)*,)*
                 {
-                    type Value = $tyorig < (K, V) >;
+                    type Value = $tyorig < (K, V) $(, $typaram)* >;
 
                     fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                         formatter.write_str("a map")
@@ -676,7 +661,7 @@ macro_rules! tuple_seq_as_map_impl_intern {
                     }
                 }
 
-                let visitor = MapVisitor::<K, KAs, V, VAs> {
+                let visitor = MapVisitor::<K, KAs, V, VAs $(, $typaram)*> {
                     marker: PhantomData,
                 };
                 deserializer.deserialize_map(visitor)
@@ -686,12 +671,16 @@ macro_rules! tuple_seq_as_map_impl_intern {
 }
 #[cfg(feature = "alloc")]
 macro_rules! tuple_seq_as_map_impl {
-    ($tyorig:ident < (K, V) $(: $($bound:ident $(+)?)+)?>) => {
-        tuple_seq_as_map_impl_intern!($tyorig < (K, V) $(: $($bound +)+)? >, Map<KAs, VAs>);
+    (
+        $tyorig:ident < T $(: $($bound:ident $(+)?)+)? $(, $typaram:ident : $bound1:ident $(+ $bound2:ident)* )* >,
+        $with_capacity:expr,
+        $append:ident
+    ) => {
+        tuple_seq_as_map_impl_intern!($tyorig < (K, V) $(: $($bound +)+)? $(, $typaram: $bound1 $(+ $bound2)*)*>, $with_capacity, $append, Map<KAs, VAs>);
         #[cfg(feature = "alloc")]
-        tuple_seq_as_map_impl_intern!($tyorig < (K, V) $(: $($bound +)+)? >, BTreeMap<KAs, VAs>);
+        tuple_seq_as_map_impl_intern!($tyorig < (K, V) $(: $($bound +)+)? $(, $typaram: $bound1 $(+ $bound2)*)*>, $with_capacity, $append, BTreeMap<KAs, VAs>);
         #[cfg(feature = "std")]
-        tuple_seq_as_map_impl_intern!($tyorig < (K, V) $(: $($bound +)+)? >, HashMap<KAs, VAs>);
+        tuple_seq_as_map_impl_intern!($tyorig < (K, V) $(: $($bound +)+)? $(, $typaram: $bound1 $(+ $bound2)*)*>, $with_capacity, $append, HashMap<KAs, VAs>);
     }
 }
 foreach_seq!(tuple_seq_as_map_impl);
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 b5d1c70..715c215 100644
--- a/serde_with/src/duplicate_key_impls/error_on_duplicate.rs
+++ b/serde_with/src/duplicate_key_impls/error_on_duplicate.rs
@@ -34,6 +34,26 @@ where
     }
 }
 
+#[cfg(feature = "hashbrown_0_14")]
+impl<T, S> PreventDuplicateInsertsSet<T> for hashbrown_0_14::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
@@ -109,6 +129,26 @@ where
     }
 }
 
+#[cfg(feature = "hashbrown_0_14")]
+impl<K, V, S> PreventDuplicateInsertsMap<K, V> for hashbrown_0_14::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 86c4cef..e2413e6 100644
--- a/serde_with/src/duplicate_key_impls/first_value_wins.rs
+++ b/serde_with/src/duplicate_key_impls/first_value_wins.rs
@@ -35,6 +35,34 @@ where
     }
 }
 
+#[cfg(feature = "hashbrown_0_14")]
+impl<K, V, S> DuplicateInsertsFirstWinsMap<K, V> for hashbrown_0_14::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_14::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 03f6c0f..970197a 100644
--- a/serde_with/src/duplicate_key_impls/last_value_wins.rs
+++ b/serde_with/src/duplicate_key_impls/last_value_wins.rs
@@ -28,6 +28,27 @@ where
     }
 }
 
+#[cfg(feature = "hashbrown_0_14")]
+impl<T, S> DuplicateInsertsLastWinsSet<T> for hashbrown_0_14::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/guide/feature_flags.md b/serde_with/src/guide/feature_flags.md
index 54d79c6..201625d 100644
--- a/serde_with/src/guide/feature_flags.md
+++ b/serde_with/src/guide/feature_flags.md
@@ -11,12 +11,13 @@ Using it in a `no_std` environment requires using `default-features = false`, si
 3. [`base64`](#base64)
 4. [`chrono_0_4`](#chrono_0_4)
 5. [`guide`](#guide)
-6. [`hex`](#hex)
-7. [`indexmap_1`](#indexmap_1)
-8. [`indexmap_2`](#indexmap_2)
-9. [`json`](#json)
-10. [`macros` (default)](#macros-default)
-11. [`time_0_3`](#time_0_3)
+6. [`hashbrown_0_14`](#hashbrown_0_14)
+7. [`hex`](#hex)
+8. [`indexmap_1`](#indexmap_1)
+9. [`indexmap_2`](#indexmap_2)
+10. [`json`](#json)
+11. [`macros` (default)](#macros-default)
+12. [`time_0_3`](#time_0_3)
 
 ## `alloc`
 
@@ -49,6 +50,14 @@ This pulls in `chrono` v0.4 as a dependency.
 The `guide` feature enables inclusion of this user guide.
 The feature only changes the rustdoc output and enables no other effects.
 
+## `hashbrown_0_14`
+
+The `hashbrown_0_14` feature enables `hashbown::{HashMap, HashSet}` as supported containers.
+
+This pulls in `hashbrown` v0.14 as a dependency.
+It enables the `alloc` feature.
+Some functionality is only available when `std` is enabled too.
+
 ## `hex`
 
 The `hex` feature enables serializing data in hex format.
diff --git a/serde_with/src/ser/duplicates.rs b/serde_with/src/ser/duplicates.rs
index 788dec0..fb765d5 100644
--- a/serde_with/src/ser/duplicates.rs
+++ b/serde_with/src/ser/duplicates.rs
@@ -2,6 +2,8 @@ use super::impls::{foreach_map, foreach_set};
 use crate::{
     prelude::*, MapFirstKeyWins, MapPreventDuplicates, SetLastValueWins, SetPreventDuplicates,
 };
+#[cfg(feature = "hashbrown_0_14")]
+use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014};
 #[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 b03b8b4..c6c37dd 100644
--- a/serde_with/src/ser/impls.rs
+++ b/serde_with/src/ser/impls.rs
@@ -1,4 +1,6 @@
 use crate::{formats, formats::Strictness, prelude::*};
+#[cfg(feature = "hashbrown_0_14")]
+use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014};
 #[cfg(feature = "indexmap_1")]
 use indexmap_1::{IndexMap, IndexSet};
 #[cfg(feature = "indexmap_2")]
@@ -17,9 +19,11 @@ macro_rules! foreach_map {
         $m!(BTreeMap<K, V>);
         #[cfg(feature = "std")]
         $m!(HashMap<K, V, H: Sized>);
-        #[cfg(all(feature = "indexmap_1"))]
+        #[cfg(feature = "hashbrown_0_14")]
+        $m!(HashbrownMap014<K, V, H: Sized>);
+        #[cfg(feature = "indexmap_1")]
         $m!(IndexMap<K, V, H: Sized>);
-        #[cfg(all(feature = "indexmap_2"))]
+        #[cfg(feature = "indexmap_2")]
         $m!(IndexMap2<K, V, H: Sized>);
     };
 }
@@ -31,9 +35,11 @@ macro_rules! foreach_set {
         $m!(BTreeSet<$T>);
         #[cfg(feature = "std")]
         $m!(HashSet<$T, H: Sized>);
-        #[cfg(all(feature = "indexmap_1"))]
+        #[cfg(feature = "hashbrown_0_14")]
+        $m!(HashbrownSet014<$T, H: Sized>);
+        #[cfg(feature = "indexmap_1")]
         $m!(IndexSet<$T, H: Sized>);
-        #[cfg(all(feature = "indexmap_2"))]
+        #[cfg(feature = "indexmap_2")]
         $m!(IndexSet2<$T, H: Sized>);
     };
     ($m:ident) => {


Test patch:
diff --git a/serde_with/tests/hashbrown_0_14.rs b/serde_with/tests/hashbrown_0_14.rs
new file mode 100644
index 0000000..9698e39
--- /dev/null
+++ b/serde_with/tests/hashbrown_0_14.rs
@@ -0,0 +1,302 @@
+#![allow(
+  // clippy is broken and shows wrong warnings
+  // clippy on stable does not know yet about the lint name
+  unknown_lints,
+  // https://github.com/rust-lang/rust-clippy/issues/8867
+  clippy::derive_partial_eq_without_eq,
+)]
+
+mod utils;
+
+use crate::utils::{check_deserialization, check_error_deserialization, is_equal};
+use core::iter::FromIterator;
+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_14::HashMap<K, V, S>;
+type HashSet<V, S = fnv::FnvBuildHasher> = hashbrown_0_14::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().cloned().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().cloned().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().cloned().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().cloned().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"#]],
+    );
+}