This is an automated email from the ASF dual-hosted git repository.

kriskras99 pushed a commit to branch feat/serde_transparent
in repository https://gitbox.apache.org/repos/asf/avro-rs.git

commit faf2db64ea7b0046bdb8900bc6f6966af649fa8a
Author: Kriskras99 <[email protected]>
AuthorDate: Sat Jan 10 20:40:16 2026 +0100

    feat(derive): Support `#[serde(transparent)]`
---
 avro_derive/src/attributes/mod.rs                  |  22 ++-
 avro_derive/src/lib.rs                             | 200 +++++++++++++++------
 avro_derive/tests/serde.rs                         |  22 +++
 avro_derive/tests/ui/avro_rs_373_transparent.rs    |   5 +-
 .../tests/ui/avro_rs_373_transparent.stderr        |  22 ++-
 5 files changed, 198 insertions(+), 73 deletions(-)

diff --git a/avro_derive/src/attributes/mod.rs 
b/avro_derive/src/attributes/mod.rs
index 6f26a62..d7de7da 100644
--- a/avro_derive/src/attributes/mod.rs
+++ b/avro_derive/src/attributes/mod.rs
@@ -30,6 +30,7 @@ pub struct NamedTypeOptions {
     pub doc: Option<String>,
     pub alias: Vec<String>,
     pub rename_all: RenameRule,
+    pub transparent: bool,
 }
 
 impl NamedTypeOptions {
@@ -63,12 +64,6 @@ impl NamedTypeOptions {
                 "AvroSchema derive does not support the Serde `remote` 
attribute",
             ));
         }
-        if serde.transparent {
-            errors.push(syn::Error::new(
-                span,
-                "AvroSchema derive does not support Serde `transparent` 
attribute",
-            ));
-        }
         if serde.rename_all.deserialize != serde.rename_all.serialize {
             errors.push(syn::Error::new(
                 span,
@@ -89,6 +84,19 @@ impl NamedTypeOptions {
                 "#[avro(rename_all = \"..\")] must match #[serde(rename_all = 
\"..\")], it's also deprecated. Please use only `#[serde(rename_all = 
\"..\")]`",
             ));
         }
+        if serde.transparent
+            && (serde.rename.is_some()
+                || avro.namespace.is_some()
+                || avro.doc.is_some()
+                || !avro.alias.is_empty()
+                || serde.rename_all.serialize != RenameRule::None
+                || serde.rename_all.deserialize != RenameRule::None)
+        {
+            errors.push(syn::Error::new(
+                span,
+                "#[serde(transparent)] is incompatible with all other 
attributes",
+            ));
+        }
 
         if !errors.is_empty() {
             return Err(errors);
@@ -100,6 +108,7 @@ impl NamedTypeOptions {
             doc: avro.doc,
             alias: avro.alias,
             rename_all: serde.rename_all.serialize,
+            transparent: serde.transparent,
         })
     }
 }
@@ -145,6 +154,7 @@ impl VariantOptions {
     }
 }
 
+#[derive(Default, PartialEq, Eq)]
 pub struct FieldOptions {
     pub doc: Option<String>,
     pub default: Option<String>,
diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs
index 8723593..bed6a04 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -44,71 +44,85 @@ pub fn proc_macro_derive_avro_schema(input: 
proc_macro::TokenStream) -> proc_mac
 fn derive_avro_schema(input: &mut DeriveInput) -> Result<TokenStream, 
Vec<syn::Error>> {
     let named_type_options = NamedTypeOptions::new(&input.attrs, 
input.span())?;
 
-    let rename_all = named_type_options.rename_all;
-    let name = named_type_options.name.unwrap_or(input.ident.to_string());
-
-    let full_schema_name = vec![named_type_options.namespace, Some(name)]
-        .into_iter()
-        .flatten()
-        .collect::<Vec<String>>()
-        .join(".");
-    let schema_def = match &input.data {
-        syn::Data::Struct(s) => get_data_struct_schema_def(
-            &full_schema_name,
-            named_type_options
-                .doc
-                .or_else(|| extract_outer_doc(&input.attrs)),
-            named_type_options.alias,
-            rename_all,
-            s,
-            input.ident.span(),
-        )?,
-        syn::Data::Enum(e) => get_data_enum_schema_def(
-            &full_schema_name,
-            named_type_options
-                .doc
-                .or_else(|| extract_outer_doc(&input.attrs)),
-            named_type_options.alias,
-            rename_all,
-            e,
-            input.ident.span(),
-        )?,
-        _ => {
-            return Err(vec![syn::Error::new(
-                input.ident.span(),
-                "AvroSchema derive only works for structs and simple enums ",
-            )]);
-        }
-    };
-    let ident = &input.ident;
-    let (impl_generics, ty_generics, where_clause) = 
input.generics.split_for_impl();
-    Ok(quote! {
-        #[automatically_derived]
-        impl #impl_generics apache_avro::schema::derive::AvroSchemaComponent 
for #ident #ty_generics #where_clause {
-            fn get_schema_in_ctxt(named_schemas: &mut 
std::collections::HashMap<apache_avro::schema::Name, 
apache_avro::schema::Schema>, enclosing_namespace: &Option<String>) -> 
apache_avro::schema::Schema {
-                let name =  
apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to 
parse schema name {}", 
#full_schema_name)[..]).fully_qualified_name(enclosing_namespace);
-                let enclosing_namespace = &name.namespace;
-                if named_schemas.contains_key(&name) {
-                    apache_avro::schema::Schema::Ref{name: name.clone()}
-                } else {
-                    named_schemas.insert(name.clone(), 
apache_avro::schema::Schema::Ref{name: name.clone()});
+    if named_type_options.transparent {
+        let schema_def = get_transparent_data_schema_def(&input.data, 
input.span())?;
+        let ident = &input.ident;
+        let (impl_generics, ty_generics, where_clause) = 
input.generics.split_for_impl();
+        Ok(quote! {
+            #[automatically_derived]
+            impl #impl_generics 
apache_avro::schema::derive::AvroSchemaComponent for #ident #ty_generics 
#where_clause {
+                fn get_schema_in_ctxt(named_schemas: &mut 
std::collections::HashMap<apache_avro::schema::Name, 
apache_avro::schema::Schema>, enclosing_namespace: &Option<String>) -> 
apache_avro::schema::Schema {
                     #schema_def
                 }
             }
-        }
-    })
+        })
+    } else {
+        let name = named_type_options.name.unwrap_or(input.ident.to_string());
+        let full_schema_name = vec![named_type_options.namespace, Some(name)]
+            .into_iter()
+            .flatten()
+            .collect::<Vec<String>>()
+            .join(".");
+
+        let schema_def = match &input.data {
+            syn::Data::Struct(data_struct) => get_data_struct_schema_def(
+                &full_schema_name,
+                named_type_options
+                    .doc
+                    .or_else(|| extract_outer_doc(&input.attrs)),
+                named_type_options.alias,
+                named_type_options.rename_all,
+                data_struct,
+                input.ident.span(),
+            )?,
+            syn::Data::Enum(data_enum) => get_data_enum_schema_def(
+                &full_schema_name,
+                named_type_options
+                    .doc
+                    .or_else(|| extract_outer_doc(&input.attrs)),
+                named_type_options.alias,
+                named_type_options.rename_all,
+                data_enum,
+                input.ident.span(),
+            )?,
+            syn::Data::Union(_) => {
+                return Err(vec![syn::Error::new(
+                    input.ident.span(),
+                    "AvroSchema derive only works for structs and simple 
enums",
+                )]);
+            }
+        };
+        let ident = &input.ident;
+        let (impl_generics, ty_generics, where_clause) = 
input.generics.split_for_impl();
+        Ok(quote! {
+            #[automatically_derived]
+            impl #impl_generics 
apache_avro::schema::derive::AvroSchemaComponent for #ident #ty_generics 
#where_clause {
+                fn get_schema_in_ctxt(named_schemas: &mut 
std::collections::HashMap<apache_avro::schema::Name, 
apache_avro::schema::Schema>, enclosing_namespace: &Option<String>) -> 
apache_avro::schema::Schema {
+                    let name =  
apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to 
parse schema name {}", 
#full_schema_name)[..]).fully_qualified_name(enclosing_namespace);
+                    let enclosing_namespace = &name.namespace;
+                    if named_schemas.contains_key(&name) {
+                        apache_avro::schema::Schema::Ref{name: name.clone()}
+                    } else {
+                        named_schemas.insert(name.clone(), 
apache_avro::schema::Schema::Ref{name: name.clone()});
+                        #schema_def
+                    }
+                }
+            }
+        })
+    }
 }
 
+/// Generate a schema definition for a struct.
 fn get_data_struct_schema_def(
     full_schema_name: &str,
     record_doc: Option<String>,
     aliases: Vec<String>,
     rename_all: RenameRule,
-    s: &syn::DataStruct,
-    error_span: Span,
+    data_struct: &syn::DataStruct,
+    ident_span: Span,
 ) -> Result<TokenStream, Vec<syn::Error>> {
     let mut record_field_exprs = vec![];
-    match s.fields {
+    match data_struct.fields {
         syn::Fields::Named(ref a) => {
             for field in a.named.iter() {
                 let mut name = field
@@ -184,13 +198,13 @@ fn get_data_struct_schema_def(
         }
         syn::Fields::Unnamed(_) => {
             return Err(vec![syn::Error::new(
-                error_span,
+                ident_span,
                 "AvroSchema derive does not work for tuple structs",
             )]);
         }
         syn::Fields::Unit => {
             return Err(vec![syn::Error::new(
-                error_span,
+                ident_span,
                 "AvroSchema derive does not work for unit structs",
             )]);
         }
@@ -221,21 +235,26 @@ fn get_data_struct_schema_def(
     })
 }
 
+/// Generate a schema definition for a enum.
 fn get_data_enum_schema_def(
     full_schema_name: &str,
     doc: Option<String>,
     aliases: Vec<String>,
     rename_all: RenameRule,
-    e: &syn::DataEnum,
+    data_enum: &syn::DataEnum,
     error_span: Span,
 ) -> Result<TokenStream, Vec<syn::Error>> {
     let doc = preserve_optional(doc);
     let enum_aliases = preserve_vec(aliases);
-    if e.variants.iter().all(|v| syn::Fields::Unit == v.fields) {
-        let default_value = default_enum_variant(e, error_span)?;
+    if data_enum
+        .variants
+        .iter()
+        .all(|v| syn::Fields::Unit == v.fields)
+    {
+        let default_value = default_enum_variant(data_enum, error_span)?;
         let default = preserve_optional(default_value);
         let mut symbols = Vec::new();
-        for variant in &e.variants {
+        for variant in &data_enum.variants {
             let field_attrs = VariantOptions::new(&variant.attrs, 
variant.span())?;
             let name = match (field_attrs.rename, rename_all) {
                 (Some(rename), _) => rename,
@@ -264,6 +283,69 @@ fn get_data_enum_schema_def(
     }
 }
 
+/// Generate a schema definition for a type marked transparent.
+fn get_transparent_data_schema_def(
+    data: &syn::Data,
+    input_span: Span,
+) -> Result<TokenStream, Vec<syn::Error>> {
+    match data {
+        syn::Data::Struct(data_struct) => match &data_struct.fields {
+            syn::Fields::Named(fields_named) => {
+                if fields_named.named.len() != 1 {
+                    return Err(vec![syn::Error::new(
+                        input_span,
+                        "#[serde(transparent)] is only allowed on structs with 
one field",
+                    )]);
+                }
+                let field = fields_named
+                    .named
+                    .first()
+                    .expect("There is exactly one field");
+                let field_attrs = FieldOptions::new(&field.attrs, 
field.span())?;
+                if field_attrs != FieldOptions::default() {
+                    return Err(vec![syn::Error::new(
+                        input_span,
+                        "#[serde(transparent)] is incompatible with all other 
attributes",
+                    )]);
+                }
+                let ty = &field.ty;
+                Ok(quote! {
+                    #ty::get_schema_in_ctxt(named_schemas, enclosing_namespace)
+                })
+            }
+            syn::Fields::Unnamed(_) => Err(vec![syn::Error::new(
+                input_span,
+                "AvroSchema derive does not work for tuple structs",
+            )]),
+            syn::Fields::Unit => Err(vec![syn::Error::new(
+                input_span,
+                "AvroSchema derive does not work for unit structs",
+            )]),
+        },
+        syn::Data::Enum(data_enum) => {
+            if data_enum
+                .variants
+                .iter()
+                .all(|v| syn::Fields::Unit == v.fields)
+            {
+                Err(vec![syn::Error::new(
+                    input_span,
+                    "AvroSchema derive does not support #[serde(transparent)] 
on simple enums",
+                )])
+            } else {
+                Err(vec![syn::Error::new(
+                    input_span,
+                    "AvroSchema derive does not work for enums with non unit 
structs",
+                )])
+            }
+        }
+        syn::Data::Union(_) => Err(vec![syn::Error::new(
+            input_span,
+            "AvroSchema derive only works for structs and simple enums",
+        )]),
+    }
+}
+
 /// Takes in the Tokens of a type and returns the tokens of an expression with 
return type `Schema`
 fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> {
     if let Type::Path(p) = ty {
diff --git a/avro_derive/tests/serde.rs b/avro_derive/tests/serde.rs
index f42e97b..63974ef 100644
--- a/avro_derive/tests/serde.rs
+++ b/avro_derive/tests/serde.rs
@@ -314,6 +314,28 @@ mod container_attributes {
             "Invalid field name c",
         );
     }
+
+    #[test]
+    fn avro_rs_398_transparent() {
+        #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)]
+        #[serde(transparent)]
+        struct Foo {
+            a: String,
+        }
+
+        let schema = r#"
+        {
+            "type":"string"
+        }
+        "#;
+
+        let schema = Schema::parse_str(schema).unwrap();
+        assert_eq!(schema, Foo::get_schema());
+
+        serde_assert(Foo {
+            a: "spam".to_string(),
+        });
+    }
 }
 
 mod variant_attributes {
diff --git a/avro_derive/tests/ui/avro_rs_373_transparent.rs 
b/avro_derive/tests/ui/avro_rs_373_transparent.rs
index 83fe2b1..3b6ba49 100644
--- a/avro_derive/tests/ui/avro_rs_373_transparent.rs
+++ b/avro_derive/tests/ui/avro_rs_373_transparent.rs
@@ -18,6 +18,7 @@
 use apache_avro::AvroSchema;
 
 #[derive(AvroSchema)]
+#[serde(transparent)]
 struct Foo {
     a: String,
     b: i32,
@@ -25,8 +26,8 @@ struct Foo {
 
 #[derive(AvroSchema)]
 #[serde(transparent)]
-struct Bar {
-    foo: Foo,
+enum Bar {
+    A
 }
 
 pub fn main() {}
diff --git a/avro_derive/tests/ui/avro_rs_373_transparent.stderr 
b/avro_derive/tests/ui/avro_rs_373_transparent.stderr
index d300405..025f4fd 100644
--- a/avro_derive/tests/ui/avro_rs_373_transparent.stderr
+++ b/avro_derive/tests/ui/avro_rs_373_transparent.stderr
@@ -1,8 +1,18 @@
-error: AvroSchema derive does not support Serde `transparent` attribute
-  --> tests/ui/avro_rs_373_transparent.rs:27:1
+error: #[serde(transparent)] is only allowed on structs with one field
+  --> tests/ui/avro_rs_373_transparent.rs:21:1
    |
-27 | / #[serde(transparent)]
-28 | | struct Bar {
-29 | |     foo: Foo,
-30 | | }
+21 | / #[serde(transparent)]
+22 | | struct Foo {
+23 | |     a: String,
+24 | |     b: i32,
+25 | | }
+   | |_^
+
+error: AvroSchema derive does not support #[serde(transparent)] on simple enums
+  --> tests/ui/avro_rs_373_transparent.rs:28:1
+   |
+28 | / #[serde(transparent)]
+29 | | enum Bar {
+30 | |     A
+31 | | }
    | |_^

Reply via email to