Repository: near/borsh-rs (rust)

Issue:
`BorshSerialize` failure to compile derive with `#[borsh_skip]` on struct variant field
While resolving #110 , the following snippet fails to compile (in the `BorshSerialize` part). 
It must be more or less obvious bug, as derive of `BorshDeserialize` succeeds (as well as `BorshSerialize` for tuple struct variants).
```rust
use borsh::{self, BorshSerialize, BorshDeserialize};


#[derive(BorshDeserialize)]
enum AA {
    B {
        #[borsh_skip]
        c: i32,

        d: u32,
    },

    NegatedVariant {
        beta: u8,
    }
}

#[derive(BorshDeserialize)]
enum AAT {
    B(#[borsh_skip] i32, u32),

    NegatedVariant {
        beta: u8,
    }
}

#[derive(BorshSerialize)]
enum AATB {
    B(#[borsh_skip] i32, u32),

    NegatedVariant {
        beta: u8,
    }
}

#[derive(BorshSerialize)]
enum AATTB {
    B(#[borsh_skip] i32, #[borsh_skip] u32),

    NegatedVariant {
        beta: u8,
    }
}
// ERROR!
#[derive(BorshSerialize)]
enum AB {
    B {
        #[borsh_skip]
        c: i32,

        d: u32,
    },

    NegatedVariant {
        beta: String,
    }
}
// ERROR!
#[derive(BorshSerialize)]
enum AAB {
    B {
        #[borsh_skip]
        c: i32,

        #[borsh_skip]
        d: u32,
    },

    NegatedVariant {
        beta: String,
    }
}


fn main() {
    
}
```

```bash
 1  error: expected field pattern, found `_`
   --> src/main.rs:44:10
 2  error: expected `}`, found `c`
   --> src/main.rs:48:9
 3  error: proc-macro derive produced unparsable tokens
   --> src/main.rs:44:10
 4  error: expected field pattern, found `_`
   --> src/main.rs:58:10
 5  error: expected `}`, found `c`
   --> src/main.rs:62:9
 6  error: proc-macro derive produced unparsable tokens
   --> src/main.rs:58:10
 7  error[E0425]: cannot find value `d` in this scope
   --> src/main.rs:50:9

```

Pull request description:
fix: #[borsh_skip] on field of struct enum variant (BorshSerialize)
Resolves #173 

Patch:
diff --git a/borsh-derive-internal/Cargo.toml b/borsh-derive-internal/Cargo.toml
index c330fa76..c607e6cc 100644
--- a/borsh-derive-internal/Cargo.toml
+++ b/borsh-derive-internal/Cargo.toml
@@ -17,3 +17,8 @@ Binary Object Representation Serializer for Hashing
 proc-macro2 = "1"
 syn = { version = "2", features = ["full", "fold"] }
 quote = "1"
+
+[dev-dependencies]
+syn = { version = "2", features = ["full", "fold", "parsing"] }
+prettyplease = "0.2.9"
+insta = "1.29.0"
diff --git a/borsh-derive-internal/src/enum_de.rs b/borsh-derive-internal/src/enum_de.rs
index 1e5dc516..678716e5 100644
--- a/borsh-derive-internal/src/enum_de.rs
+++ b/borsh-derive-internal/src/enum_de.rs
@@ -109,3 +109,48 @@ pub fn enum_de(input: &ItemEnum, cratename: Ident) -> syn::Result<TokenStream2>
         }
     })
 }
+
+#[cfg(test)]
+mod tests {
+    use crate::test_helpers::pretty_print_syn_str;
+
+    use super::*;
+    use proc_macro2::Span;
+
+    #[test]
+    fn borsh_skip_struct_variant_field() {
+        let item_enum: ItemEnum = syn::parse2(quote! {
+            enum AA {
+                B {
+                    #[borsh_skip]
+                    c: i32,
+                    d: u32,
+                },
+                NegatedVariant {
+                    beta: u8,
+                }
+            }
+        })
+        .unwrap();
+        let actual = enum_de(&item_enum, Ident::new("borsh", Span::call_site())).unwrap();
+
+        insta::assert_snapshot!(pretty_print_syn_str(&actual).unwrap());
+    }
+
+    #[test]
+    fn borsh_skip_tuple_variant_field() {
+        let item_enum: ItemEnum = syn::parse2(quote! {
+            enum AAT {
+                B(#[borsh_skip] i32, u32),
+
+                NegatedVariant {
+                    beta: u8,
+                }
+            }
+        })
+        .unwrap();
+        let actual = enum_de(&item_enum, Ident::new("borsh", Span::call_site())).unwrap();
+
+        insta::assert_snapshot!(pretty_print_syn_str(&actual).unwrap());
+    }
+}
diff --git a/borsh-derive-internal/src/enum_ser.rs b/borsh-derive-internal/src/enum_ser.rs
index 3a9c29e6..e15be331 100644
--- a/borsh-derive-internal/src/enum_ser.rs
+++ b/borsh-derive-internal/src/enum_ser.rs
@@ -2,12 +2,12 @@ use core::convert::TryFrom;
 
 use proc_macro2::{Span, TokenStream as TokenStream2};
 use quote::quote;
-use syn::{Fields, Ident, ItemEnum, WhereClause};
+use syn::{Fields, FieldsNamed, FieldsUnnamed, Ident, ItemEnum, WhereClause, WherePredicate};
 
 use crate::{attribute_helpers::contains_skip, enum_discriminant_map::discriminant_map};
 
 pub fn enum_ser(input: &ItemEnum, cratename: Ident) -> syn::Result<TokenStream2> {
-    let name = &input.ident;
+    let enum_ident = &input.ident;
     let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
     let mut where_clause = where_clause.map_or_else(
         || WhereClause {
@@ -16,88 +16,59 @@ pub fn enum_ser(input: &ItemEnum, cratename: Ident) -> syn::Result<TokenStream2>
         },
         Clone::clone,
     );
-    let mut variant_idx_body = TokenStream2::new();
+    let mut all_variants_idx_body = TokenStream2::new();
     let mut fields_body = TokenStream2::new();
     let discriminants = discriminant_map(&input.variants);
     for variant in input.variants.iter() {
         let variant_ident = &variant.ident;
-        let mut variant_header = TokenStream2::new();
-        let mut variant_body = TokenStream2::new();
         let discriminant_value = discriminants.get(variant_ident).unwrap();
-        match &variant.fields {
-            Fields::Named(fields) => {
-                for field in &fields.named {
-                    let field_name = field.ident.as_ref().unwrap();
-                    if contains_skip(&field.attrs) {
-                        variant_header.extend(quote! { _#field_name, });
-                        continue;
-                    } else {
-                        let field_type = &field.ty;
-                        where_clause.predicates.push(
-                            syn::parse2(quote! {
-                                #field_type: #cratename::ser::BorshSerialize
-                            })
-                            .unwrap(),
-                        );
-                        variant_header.extend(quote! { #field_name, });
-                    }
-                    variant_body.extend(quote! {
-                         #cratename::BorshSerialize::serialize(#field_name, writer)?;
-                    })
-                }
-                variant_header = quote! { { #variant_header }};
-                variant_idx_body.extend(quote!(
-                    #name::#variant_ident { .. } => #discriminant_value,
-                ));
-            }
-            Fields::Unnamed(fields) => {
-                for (field_idx, field) in fields.unnamed.iter().enumerate() {
-                    let field_idx =
-                        u32::try_from(field_idx).expect("up to 2^32 fields are supported");
-                    if contains_skip(&field.attrs) {
-                        let field_ident =
-                            Ident::new(format!("_id{}", field_idx).as_str(), Span::call_site());
-                        variant_header.extend(quote! { #field_ident, });
-                        continue;
-                    } else {
-                        let field_type = &field.ty;
-                        where_clause.predicates.push(
-                            syn::parse2(quote! {
-                                #field_type: #cratename::ser::BorshSerialize
-                            })
-                            .unwrap(),
-                        );
-
-                        let field_ident =
-                            Ident::new(format!("id{}", field_idx).as_str(), Span::call_site());
-                        variant_header.extend(quote! { #field_ident, });
-                        variant_body.extend(quote! {
-                            #cratename::BorshSerialize::serialize(#field_ident, writer)?;
-                        })
-                    }
-                }
-                variant_header = quote! { ( #variant_header )};
-                variant_idx_body.extend(quote!(
-                    #name::#variant_ident(..) => #discriminant_value,
-                ));
-            }
+        let VariantParts {
+            where_predicates,
+            variant_header,
+            variant_body,
+            variant_idx_body,
+        } = match &variant.fields {
+            Fields::Named(fields) => named_fields(
+                &cratename,
+                enum_ident,
+                variant_ident,
+                discriminant_value,
+                fields,
+            )?,
+            Fields::Unnamed(fields) => unnamed_fields(
+                &cratename,
+                enum_ident,
+                variant_ident,
+                discriminant_value,
+                fields,
+            )?,
             Fields::Unit => {
-                variant_idx_body.extend(quote!(
-                    #name::#variant_ident => #discriminant_value,
-                ));
+                let variant_idx_body = quote!(
+                    #enum_ident::#variant_ident => #discriminant_value,
+                );
+                VariantParts {
+                    where_predicates: vec![],
+                    variant_header: TokenStream2::new(),
+                    variant_body: TokenStream2::new(),
+                    variant_idx_body,
+                }
             }
-        }
+        };
+        where_predicates
+            .into_iter()
+            .for_each(|predicate| where_clause.predicates.push(predicate));
+        all_variants_idx_body.extend(variant_idx_body);
         fields_body.extend(quote!(
-            #name::#variant_ident #variant_header => {
+            #enum_ident::#variant_ident #variant_header => {
                 #variant_body
             }
         ))
     }
     Ok(quote! {
-        impl #impl_generics #cratename::ser::BorshSerialize for #name #ty_generics #where_clause {
+        impl #impl_generics #cratename::ser::BorshSerialize for #enum_ident #ty_generics #where_clause {
             fn serialize<W: #cratename::__private::maybestd::io::Write>(&self, writer: &mut W) -> ::core::result::Result<(), #cratename::__private::maybestd::io::Error> {
                 let variant_idx: u8 = match self {
-                    #variant_idx_body
+                    #all_variants_idx_body
                 };
                 writer.write_all(&variant_idx.to_le_bytes())?;
 
@@ -109,3 +80,190 @@ pub fn enum_ser(input: &ItemEnum, cratename: Ident) -> syn::Result<TokenStream2>
         }
     })
 }
+
+struct VariantParts {
+    where_predicates: Vec<WherePredicate>,
+    variant_header: TokenStream2,
+    variant_body: TokenStream2,
+    variant_idx_body: TokenStream2,
+}
+fn named_fields(
+    cratename: &Ident,
+    enum_ident: &Ident,
+    variant_ident: &Ident,
+    discriminant_value: &TokenStream2,
+    fields: &FieldsNamed,
+) -> syn::Result<VariantParts> {
+    let mut where_predicates: Vec<WherePredicate> = vec![];
+    let mut variant_header = TokenStream2::new();
+    let mut variant_body = TokenStream2::new();
+    for field in &fields.named {
+        if !contains_skip(&field.attrs) {
+            let field_ident = field.ident.clone().unwrap();
+
+            variant_header.extend(quote! { #field_ident, });
+
+            let field_type = &field.ty;
+            where_predicates.push(
+                syn::parse2(quote! {
+                    #field_type: #cratename::ser::BorshSerialize
+                })
+                .unwrap(),
+            );
+
+            variant_body.extend(quote! {
+                 #cratename::BorshSerialize::serialize(#field_ident, writer)?;
+            })
+        }
+    }
+    // `..` pattern matching works even if all fields were specified
+    variant_header = quote! { { #variant_header .. }};
+    let variant_idx_body = quote!(
+        #enum_ident::#variant_ident { .. } => #discriminant_value,
+    );
+    Ok(VariantParts {
+        where_predicates,
+        variant_header,
+        variant_body,
+        variant_idx_body,
+    })
+}
+
+fn unnamed_fields(
+    cratename: &Ident,
+    enum_ident: &Ident,
+    variant_ident: &Ident,
+    discriminant_value: &TokenStream2,
+    fields: &FieldsUnnamed,
+) -> syn::Result<VariantParts> {
+    let mut where_predicates: Vec<WherePredicate> = vec![];
+    let mut variant_header = TokenStream2::new();
+    let mut variant_body = TokenStream2::new();
+    for (field_idx, field) in fields.unnamed.iter().enumerate() {
+        let field_idx = u32::try_from(field_idx).expect("up to 2^32 fields are supported");
+        if contains_skip(&field.attrs) {
+            let field_ident = Ident::new(format!("_id{}", field_idx).as_str(), Span::mixed_site());
+            variant_header.extend(quote! { #field_ident, });
+        } else {
+            let field_ident = Ident::new(format!("id{}", field_idx).as_str(), Span::mixed_site());
+
+            variant_header.extend(quote! { #field_ident, });
+
+            let field_type = &field.ty;
+            where_predicates.push(
+                syn::parse2(quote! {
+                    #field_type: #cratename::ser::BorshSerialize
+                })
+                .unwrap(),
+            );
+
+            variant_body.extend(quote! {
+                #cratename::BorshSerialize::serialize(#field_ident, writer)?;
+            })
+        }
+    }
+    variant_header = quote! { ( #variant_header )};
+    let variant_idx_body = quote!(
+        #enum_ident::#variant_ident(..) => #discriminant_value,
+    );
+    Ok(VariantParts {
+        where_predicates,
+        variant_header,
+        variant_body,
+        variant_idx_body,
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::test_helpers::pretty_print_syn_str;
+
+    use super::*;
+    use proc_macro2::Span;
+    #[test]
+    fn borsh_skip_tuple_variant_field() {
+        let item_enum: ItemEnum = syn::parse2(quote! {
+            enum AATTB {
+                B(#[borsh_skip] i32, #[borsh_skip] u32),
+
+                NegatedVariant {
+                    beta: u8,
+                }
+            }
+        })
+        .unwrap();
+        let actual = enum_ser(&item_enum, Ident::new("borsh", Span::call_site())).unwrap();
+
+        insta::assert_snapshot!(pretty_print_syn_str(&actual).unwrap());
+    }
+
+    #[test]
+    fn struct_variant_field() {
+        let item_enum: ItemEnum = syn::parse2(quote! {
+            enum AB {
+                B {
+                    c: i32,
+                    d: u32,
+                },
+
+                NegatedVariant {
+                    beta: String,
+                }
+            }
+        })
+        .unwrap();
+
+        let actual = enum_ser(&item_enum, Ident::new("borsh", Span::call_site())).unwrap();
+
+        insta::assert_snapshot!(pretty_print_syn_str(&actual).unwrap());
+    }
+
+    #[test]
+    fn borsh_skip_struct_variant_field() {
+        let item_enum: ItemEnum = syn::parse2(quote! {
+
+            enum AB {
+                B {
+                    #[borsh_skip]
+                    c: i32,
+
+                    d: u32,
+                },
+
+                NegatedVariant {
+                    beta: String,
+                }
+            }
+        })
+        .unwrap();
+
+        let actual = enum_ser(&item_enum, Ident::new("borsh", Span::call_site())).unwrap();
+
+        insta::assert_snapshot!(pretty_print_syn_str(&actual).unwrap());
+    }
+
+    #[test]
+    fn borsh_skip_struct_variant_all_fields() {
+        let item_enum: ItemEnum = syn::parse2(quote! {
+
+            enum AAB {
+                B {
+                    #[borsh_skip]
+                    c: i32,
+
+                    #[borsh_skip]
+                    d: u32,
+                },
+
+                NegatedVariant {
+                    beta: String,
+                }
+            }
+        })
+        .unwrap();
+
+        let actual = enum_ser(&item_enum, Ident::new("borsh", Span::call_site())).unwrap();
+
+        insta::assert_snapshot!(pretty_print_syn_str(&actual).unwrap());
+    }
+}
diff --git a/borsh-derive-internal/src/lib.rs b/borsh-derive-internal/src/lib.rs
index 47f5eb99..469d73b2 100644
--- a/borsh-derive-internal/src/lib.rs
+++ b/borsh-derive-internal/src/lib.rs
@@ -15,3 +15,6 @@ pub use struct_de::struct_de;
 pub use struct_ser::struct_ser;
 pub use union_de::union_de;
 pub use union_ser::union_ser;
+
+#[cfg(test)]
+pub mod test_helpers;


Test patch:
diff --git a/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_de__tests__borsh_skip_struct_variant_field.snap b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_de__tests__borsh_skip_struct_variant_field.snap
new file mode 100644
index 00000000..fbc56a72
--- /dev/null
+++ b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_de__tests__borsh_skip_struct_variant_field.snap
@@ -0,0 +1,48 @@
+---
+source: borsh-derive-internal/src/enum_de.rs
+expression: pretty_print_syn_str(&actual)
+---
+impl borsh::de::BorshDeserialize for AA
+where
+    u32: borsh::BorshDeserialize,
+    u8: borsh::BorshDeserialize,
+{
+    fn deserialize_reader<R: borsh::__private::maybestd::io::Read>(
+        reader: &mut R,
+    ) -> ::core::result::Result<Self, borsh::__private::maybestd::io::Error> {
+        let tag = <u8 as borsh::de::BorshDeserialize>::deserialize_reader(reader)?;
+        <Self as borsh::de::EnumExt>::deserialize_variant(reader, tag)
+    }
+}
+impl borsh::de::EnumExt for AA
+where
+    u32: borsh::BorshDeserialize,
+    u8: borsh::BorshDeserialize,
+{
+    fn deserialize_variant<R: borsh::__private::maybestd::io::Read>(
+        reader: &mut R,
+        variant_tag: u8,
+    ) -> ::core::result::Result<Self, borsh::__private::maybestd::io::Error> {
+        let mut return_value = if variant_tag == 0 {
+            AA::B {
+                c: Default::default(),
+                d: borsh::BorshDeserialize::deserialize_reader(reader)?,
+            }
+        } else if variant_tag == 0 + 1 {
+            AA::NegatedVariant {
+                beta: borsh::BorshDeserialize::deserialize_reader(reader)?,
+            }
+        } else {
+            return Err(
+                borsh::__private::maybestd::io::Error::new(
+                    borsh::__private::maybestd::io::ErrorKind::InvalidData,
+                    borsh::__private::maybestd::format!(
+                        "Unexpected variant tag: {:?}", variant_tag
+                    ),
+                ),
+            )
+        };
+        Ok(return_value)
+    }
+}
+
diff --git a/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_de__tests__borsh_skip_tuple_variant_field.snap b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_de__tests__borsh_skip_tuple_variant_field.snap
new file mode 100644
index 00000000..701713d6
--- /dev/null
+++ b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_de__tests__borsh_skip_tuple_variant_field.snap
@@ -0,0 +1,48 @@
+---
+source: borsh-derive-internal/src/enum_de.rs
+expression: pretty_print_syn_str(&actual)
+---
+impl borsh::de::BorshDeserialize for AAT
+where
+    u32: borsh::BorshDeserialize,
+    u8: borsh::BorshDeserialize,
+{
+    fn deserialize_reader<R: borsh::__private::maybestd::io::Read>(
+        reader: &mut R,
+    ) -> ::core::result::Result<Self, borsh::__private::maybestd::io::Error> {
+        let tag = <u8 as borsh::de::BorshDeserialize>::deserialize_reader(reader)?;
+        <Self as borsh::de::EnumExt>::deserialize_variant(reader, tag)
+    }
+}
+impl borsh::de::EnumExt for AAT
+where
+    u32: borsh::BorshDeserialize,
+    u8: borsh::BorshDeserialize,
+{
+    fn deserialize_variant<R: borsh::__private::maybestd::io::Read>(
+        reader: &mut R,
+        variant_tag: u8,
+    ) -> ::core::result::Result<Self, borsh::__private::maybestd::io::Error> {
+        let mut return_value = if variant_tag == 0 {
+            AAT::B(
+                Default::default(),
+                borsh::BorshDeserialize::deserialize_reader(reader)?,
+            )
+        } else if variant_tag == 0 + 1 {
+            AAT::NegatedVariant {
+                beta: borsh::BorshDeserialize::deserialize_reader(reader)?,
+            }
+        } else {
+            return Err(
+                borsh::__private::maybestd::io::Error::new(
+                    borsh::__private::maybestd::io::ErrorKind::InvalidData,
+                    borsh::__private::maybestd::format!(
+                        "Unexpected variant tag: {:?}", variant_tag
+                    ),
+                ),
+            )
+        };
+        Ok(return_value)
+    }
+}
+
diff --git a/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__borsh_skip_struct_variant_all_fields.snap b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__borsh_skip_struct_variant_all_fields.snap
new file mode 100644
index 00000000..af12ebad
--- /dev/null
+++ b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__borsh_skip_struct_variant_all_fields.snap
@@ -0,0 +1,27 @@
+---
+source: borsh-derive-internal/src/enum_ser.rs
+expression: pretty_print_syn_str(&actual).unwrap()
+---
+impl borsh::ser::BorshSerialize for AAB
+where
+    String: borsh::ser::BorshSerialize,
+{
+    fn serialize<W: borsh::__private::maybestd::io::Write>(
+        &self,
+        writer: &mut W,
+    ) -> ::core::result::Result<(), borsh::__private::maybestd::io::Error> {
+        let variant_idx: u8 = match self {
+            AAB::B { .. } => 0,
+            AAB::NegatedVariant { .. } => 0 + 1,
+        };
+        writer.write_all(&variant_idx.to_le_bytes())?;
+        match self {
+            AAB::B { .. } => {}
+            AAB::NegatedVariant { beta, .. } => {
+                borsh::BorshSerialize::serialize(beta, writer)?;
+            }
+        }
+        Ok(())
+    }
+}
+
diff --git a/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__borsh_skip_struct_variant_field.snap b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__borsh_skip_struct_variant_field.snap
new file mode 100644
index 00000000..414445e7
--- /dev/null
+++ b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__borsh_skip_struct_variant_field.snap
@@ -0,0 +1,30 @@
+---
+source: borsh-derive-internal/src/enum_ser.rs
+expression: pretty_print_syn_str(&actual).unwrap()
+---
+impl borsh::ser::BorshSerialize for AB
+where
+    u32: borsh::ser::BorshSerialize,
+    String: borsh::ser::BorshSerialize,
+{
+    fn serialize<W: borsh::__private::maybestd::io::Write>(
+        &self,
+        writer: &mut W,
+    ) -> ::core::result::Result<(), borsh::__private::maybestd::io::Error> {
+        let variant_idx: u8 = match self {
+            AB::B { .. } => 0,
+            AB::NegatedVariant { .. } => 0 + 1,
+        };
+        writer.write_all(&variant_idx.to_le_bytes())?;
+        match self {
+            AB::B { d, .. } => {
+                borsh::BorshSerialize::serialize(d, writer)?;
+            }
+            AB::NegatedVariant { beta, .. } => {
+                borsh::BorshSerialize::serialize(beta, writer)?;
+            }
+        }
+        Ok(())
+    }
+}
+
diff --git a/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__borsh_skip_tuple_variant_field.snap b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__borsh_skip_tuple_variant_field.snap
new file mode 100644
index 00000000..4b9eb540
--- /dev/null
+++ b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__borsh_skip_tuple_variant_field.snap
@@ -0,0 +1,27 @@
+---
+source: borsh-derive-internal/src/enum_ser.rs
+expression: pretty_print_syn_str(&actual).unwrap()
+---
+impl borsh::ser::BorshSerialize for AATTB
+where
+    u8: borsh::ser::BorshSerialize,
+{
+    fn serialize<W: borsh::__private::maybestd::io::Write>(
+        &self,
+        writer: &mut W,
+    ) -> ::core::result::Result<(), borsh::__private::maybestd::io::Error> {
+        let variant_idx: u8 = match self {
+            AATTB::B(..) => 0,
+            AATTB::NegatedVariant { .. } => 0 + 1,
+        };
+        writer.write_all(&variant_idx.to_le_bytes())?;
+        match self {
+            AATTB::B(_id0, _id1) => {}
+            AATTB::NegatedVariant { beta, .. } => {
+                borsh::BorshSerialize::serialize(beta, writer)?;
+            }
+        }
+        Ok(())
+    }
+}
+
diff --git a/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__struct_variant_field.snap b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__struct_variant_field.snap
new file mode 100644
index 00000000..cf215b58
--- /dev/null
+++ b/borsh-derive-internal/src/snapshots/borsh_derive_internal__enum_ser__tests__struct_variant_field.snap
@@ -0,0 +1,32 @@
+---
+source: borsh-derive-internal/src/enum_ser.rs
+expression: pretty_print_syn_str(&actual).unwrap()
+---
+impl borsh::ser::BorshSerialize for AB
+where
+    i32: borsh::ser::BorshSerialize,
+    u32: borsh::ser::BorshSerialize,
+    String: borsh::ser::BorshSerialize,
+{
+    fn serialize<W: borsh::__private::maybestd::io::Write>(
+        &self,
+        writer: &mut W,
+    ) -> ::core::result::Result<(), borsh::__private::maybestd::io::Error> {
+        let variant_idx: u8 = match self {
+            AB::B { .. } => 0,
+            AB::NegatedVariant { .. } => 0 + 1,
+        };
+        writer.write_all(&variant_idx.to_le_bytes())?;
+        match self {
+            AB::B { c, d, .. } => {
+                borsh::BorshSerialize::serialize(c, writer)?;
+                borsh::BorshSerialize::serialize(d, writer)?;
+            }
+            AB::NegatedVariant { beta, .. } => {
+                borsh::BorshSerialize::serialize(beta, writer)?;
+            }
+        }
+        Ok(())
+    }
+}
+
diff --git a/borsh-derive-internal/src/test_helpers.rs b/borsh-derive-internal/src/test_helpers.rs
new file mode 100644
index 00000000..6c4d17a3
--- /dev/null
+++ b/borsh-derive-internal/src/test_helpers.rs
@@ -0,0 +1,9 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+
+pub fn pretty_print_syn_str(input: &TokenStream) -> syn::Result<String> {
+    let input = format!("{}", quote!(#input));
+    let syn_file = syn::parse_str::<syn::File>(&input)?;
+
+    Ok(prettyplease::unparse(&syn_file))
+}