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

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

commit 79b30c71ee879ccdca0ad00875f8d1c084bd6396
Author: Kriskras99 <[email protected]>
AuthorDate: Wed Jan 28 22:44:38 2026 +0100

    fix: `flatten` no longer causes duplicate names
    
    When a type was used both via `flatten` and directly, the schema generated 
would
    contain duplicate names (and schemas). This is because `flatten` would use 
an empty
    `named_schemas` to get the record schema. If the existing `named_schemas` 
was used,
    `flatten` might get a `Schema::Ref` if the type was already used. Or when 
`flatten`
    was used first, if the type was used after it would create a `Schema::Ref` 
to a schema
    that does not exist.
    
    This is solved by adding a new function to the `AvroSchemaComponent` that 
returns
    the fields directly. To not break code currently implementing this trait, 
it has
    a default implementation that will work around the issues above. This 
default
    implementation is also used for fields with the `#[avro(with = ||)]` and 
`#[avro(with = path)]`
    attributes, as they don't have a way to provide the field directly. Users 
of `#[avro(with)]`
    will need to implement `get_record_fields_in_ctxt` in their module.
---
 avro/src/serde/derive.rs          | 205 +++++++++++++++++++++++++++++++++++++-
 avro/src/serde/mod.rs             |   3 +
 avro_derive/src/attributes/mod.rs |   2 +-
 avro_derive/src/lib.rs            | 138 ++++++++++++++++++++-----
 avro_derive/tests/derive.rs       |  62 ++++++++++++
 5 files changed, 383 insertions(+), 27 deletions(-)

diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs
index b51cee7..5abd650 100644
--- a/avro/src/serde/derive.rs
+++ b/avro/src/serde/derive.rs
@@ -16,7 +16,9 @@
 // under the License.
 
 use crate::Schema;
-use crate::schema::{FixedSchema, Name, Names, Namespace, UnionSchema, 
UuidSchema};
+use crate::schema::{
+    FixedSchema, Name, Names, Namespace, RecordField, RecordSchema, 
UnionSchema, UuidSchema,
+};
 use serde_json::Map;
 use std::borrow::Cow;
 use std::collections::HashMap;
@@ -83,6 +85,122 @@ pub trait AvroSchema {
 /// ```
 pub trait AvroSchemaComponent {
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema;
+
+    /// Get the fields of this schema if it is a record.
+    ///
+    /// This returns `None` if the schema is not a record.
+    ///
+    /// The default implementation has to do a lot of extra work, so it is 
strongly recommended to
+    /// implement this function when manually implementing this trait.
+    fn get_record_fields_in_ctxt(
+        named_schemas: &mut Names,
+        enclosing_namespace: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        get_record_fields_in_ctxt(named_schemas, enclosing_namespace, 
Self::get_schema_in_ctxt)
+    }
+}
+
+/// This is public so the derive macro can use it for `#[avro(with = ||)]` and 
`#[avro(with = path)]`
+pub fn get_record_fields_in_ctxt(
+    named_schemas: &mut Names,
+    enclosing_namespace: &Namespace,
+    schema_fn: fn(named_schemas: &mut Names, enclosing_namespace: &Namespace) 
-> Schema,
+) -> Option<Vec<RecordField>> {
+    let mut record = match schema_fn(named_schemas, enclosing_namespace) {
+        Schema::Record(record) => record,
+        Schema::Ref { name } => {
+            // This schema already exists in `named_schemas` so temporarily 
remove it so we can
+            // get the actual schema.
+            let temp = named_schemas
+                .remove(&name)
+                .expect("Name should exist in `named_schemas` otherwise Ref is 
invalid");
+            // Get the schema
+            let schema = schema_fn(named_schemas, enclosing_namespace);
+            // Reinsert the old value
+            named_schemas.insert(name, temp);
+
+            // Now check if we actually got a record and return the fields if 
that is the case
+            let Schema::Record(record) = schema else {
+                return None;
+            };
+            return Some(record.fields);
+        }
+        _ => return None,
+    };
+    // This schema did not yet exist in `named_schemas`, so we need to remove 
it if and only if
+    // it isn't used somewhere in the schema (recursive type).
+
+    // Find the first Schema::Ref that has the target name
+    fn find_first_ref<'a>(schema: &'a mut Schema, target: &Name) -> Option<&'a 
mut Schema> {
+        match schema {
+            Schema::Ref { name } if name == target => Some(schema),
+            Schema::Array(array) => find_first_ref(&mut array.items, target),
+            Schema::Map(map) => find_first_ref(&mut map.types, target),
+            Schema::Union(union) => {
+                for schema in &mut union.schemas {
+                    if let Some(schema) = find_first_ref(schema, target) {
+                        return Some(schema);
+                    }
+                }
+                None
+            }
+            Schema::Record(record) => {
+                assert_ne!(
+                    &record.name, target,
+                    "Only expecting a Ref named {target:?}"
+                );
+                for field in &mut record.fields {
+                    if let Some(schema) = find_first_ref(&mut field.schema, 
target) {
+                        return Some(schema);
+                    }
+                }
+                None
+            }
+            _ => None,
+        }
+    }
+
+    // Prepare the fields for the new record. All named types will become 
references.
+    let new_fields = record
+        .fields
+        .iter()
+        .map(|field| RecordField {
+            name: field.name.clone(),
+            doc: field.doc.clone(),
+            aliases: field.aliases.clone(),
+            default: field.default.clone(),
+            schema: if field.schema.is_named() {
+                Schema::Ref {
+                    name: field.schema.name().expect("Schema is 
named").clone(),
+                }
+            } else {
+                field.schema.clone()
+            },
+            order: field.order.clone(),
+            position: field.position,
+            custom_attributes: field.custom_attributes.clone(),
+        })
+        .collect();
+
+    // Find the first reference to this schema so we can replace it with the 
actual schema
+    for field in &mut record.fields {
+        if let Some(schema) = find_first_ref(&mut field.schema, &record.name) {
+            let new_schema = RecordSchema {
+                name: record.name,
+                aliases: record.aliases,
+                doc: record.doc,
+                fields: new_fields,
+                lookup: record.lookup,
+                attributes: record.attributes,
+            };
+
+            let _old = std::mem::replace(schema, Schema::Record(new_schema));
+
+            break;
+        }
+    }
+
+    Some(record.fields)
 }
 
 impl<T> AvroSchema for T
@@ -100,6 +218,10 @@ macro_rules! impl_schema (
             fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
                 $variant_constructor
             }
+
+            fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+                None
+            }
         }
     );
 );
@@ -125,6 +247,13 @@ where
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
     }
+
+    fn get_record_fields_in_ctxt(
+        named_schemas: &mut Names,
+        enclosing_namespace: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
+    }
 }
 
 impl<T> AvroSchemaComponent for &mut T
@@ -134,6 +263,13 @@ where
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
     }
+
+    fn get_record_fields_in_ctxt(
+        named_schemas: &mut Names,
+        enclosing_namespace: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
+    }
 }
 
 impl<T> AvroSchemaComponent for [T]
@@ -143,6 +279,10 @@ where
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         Schema::array(T::get_schema_in_ctxt(named_schemas, 
enclosing_namespace))
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl<const N: usize, T> AvroSchemaComponent for [T; N]
@@ -152,6 +292,10 @@ where
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         Schema::array(T::get_schema_in_ctxt(named_schemas, 
enclosing_namespace))
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl<T> AvroSchemaComponent for Vec<T>
@@ -161,6 +305,10 @@ where
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         Schema::array(T::get_schema_in_ctxt(named_schemas, 
enclosing_namespace))
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl<T> AvroSchemaComponent for Option<T>
@@ -177,6 +325,10 @@ where
             UnionSchema::new(variants).expect("Option<T> must produce a valid 
(non-nested) union"),
         )
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl<T> AvroSchemaComponent for Map<String, T>
@@ -186,6 +338,10 @@ where
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl<T> AvroSchemaComponent for HashMap<String, T>
@@ -195,6 +351,10 @@ where
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl<T> AvroSchemaComponent for Box<T>
@@ -204,6 +364,13 @@ where
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
     }
+
+    fn get_record_fields_in_ctxt(
+        named_schemas: &mut Names,
+        enclosing_namespace: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
+    }
 }
 
 impl<T> AvroSchemaComponent for std::sync::Mutex<T>
@@ -213,15 +380,29 @@ where
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
     }
+
+    fn get_record_fields_in_ctxt(
+        named_schemas: &mut Names,
+        enclosing_namespace: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
+    }
 }
 
 impl<T> AvroSchemaComponent for Cow<'_, T>
 where
-    T: AvroSchemaComponent + Clone,
+    T: AvroSchemaComponent + ToOwned + ?Sized,
 {
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
         T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
     }
+
+    fn get_record_fields_in_ctxt(
+        named_schemas: &mut Names,
+        enclosing_namespace: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
+    }
 }
 
 impl AvroSchemaComponent for core::time::Duration {
@@ -248,6 +429,10 @@ impl AvroSchemaComponent for core::time::Duration {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl AvroSchemaComponent for uuid::Uuid {
@@ -274,6 +459,10 @@ impl AvroSchemaComponent for uuid::Uuid {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl AvroSchemaComponent for u64 {
@@ -298,6 +487,10 @@ impl AvroSchemaComponent for u64 {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl AvroSchemaComponent for u128 {
@@ -322,6 +515,10 @@ impl AvroSchemaComponent for u128 {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl AvroSchemaComponent for i128 {
@@ -346,6 +543,10 @@ impl AvroSchemaComponent for i128 {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(_: &mut Names, _: &Namespace) -> 
Option<Vec<RecordField>> {
+        None
+    }
 }
 
 #[cfg(test)]
diff --git a/avro/src/serde/mod.rs b/avro/src/serde/mod.rs
index 9c1dea4..2a62b33 100644
--- a/avro/src/serde/mod.rs
+++ b/avro/src/serde/mod.rs
@@ -26,3 +26,6 @@ pub use de::from_value;
 pub use derive::{AvroSchema, AvroSchemaComponent};
 pub use ser::to_value;
 pub use with::{bytes, bytes_opt, fixed, fixed_opt, slice, slice_opt};
+
+#[doc(hidden)]
+pub use derive::get_record_fields_in_ctxt;
diff --git a/avro_derive/src/attributes/mod.rs 
b/avro_derive/src/attributes/mod.rs
index ecf2797..cc259f1 100644
--- a/avro_derive/src/attributes/mod.rs
+++ b/avro_derive/src/attributes/mod.rs
@@ -169,7 +169,7 @@ impl VariantOptions {
 }
 
 /// How to get the schema for this field or variant.
-#[derive(Debug, PartialEq, Default)]
+#[derive(Debug, PartialEq, Default, Clone)]
 pub enum With {
     /// Use `<T as AvroSchemaComponent>::get_schema_in_ctxt`.
     #[default]
diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs
index 8c49d05..32be337 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -48,14 +48,22 @@ fn derive_avro_schema(input: DeriveInput) -> 
Result<TokenStream, Vec<syn::Error>
     match input.data {
         syn::Data::Struct(data_struct) => {
             let named_type_options = NamedTypeOptions::new(&input.ident, 
&input.attrs, input_span)?;
-            let inner = if named_type_options.transparent {
+            let (get_schema_impl, get_record_fields_impl) = if 
named_type_options.transparent {
                 get_transparent_struct_schema_def(data_struct.fields, 
input_span)?
             } else {
-                let schema_def =
+                let (schema_def, record_fields) =
                     get_struct_schema_def(&named_type_options, data_struct, 
input.ident.span())?;
-                handle_named_schemas(named_type_options.name, schema_def)
+                (
+                    handle_named_schemas(named_type_options.name, schema_def),
+                    record_fields,
+                )
             };
-            Ok(create_trait_definition(input.ident, &input.generics, inner))
+            Ok(create_trait_definition(
+                input.ident,
+                &input.generics,
+                get_schema_impl,
+                get_record_fields_impl,
+            ))
         }
         syn::Data::Enum(data_enum) => {
             let named_type_options = NamedTypeOptions::new(&input.ident, 
&input.attrs, input_span)?;
@@ -68,7 +76,12 @@ fn derive_avro_schema(input: DeriveInput) -> 
Result<TokenStream, Vec<syn::Error>
             let schema_def =
                 get_data_enum_schema_def(&named_type_options, data_enum, 
input.ident.span())?;
             let inner = handle_named_schemas(named_type_options.name, 
schema_def);
-            Ok(create_trait_definition(input.ident, &input.generics, inner))
+            Ok(create_trait_definition(
+                input.ident,
+                &input.generics,
+                inner,
+                quote! { None },
+            ))
         }
         syn::Data::Union(_) => Err(vec![syn::Error::new(
             input_span,
@@ -81,14 +94,19 @@ fn derive_avro_schema(input: DeriveInput) -> 
Result<TokenStream, Vec<syn::Error>
 fn create_trait_definition(
     ident: Ident,
     generics: &Generics,
-    implementation: TokenStream,
+    get_schema_impl: TokenStream,
+    get_record_fields_impl: TokenStream,
 ) -> TokenStream {
     let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
     quote! {
         #[automatically_derived]
         impl #impl_generics apache_avro::AvroSchemaComponent for #ident 
#ty_generics #where_clause {
             fn get_schema_in_ctxt(named_schemas: &mut 
apache_avro::schema::Names, enclosing_namespace: &Option<String>) -> 
apache_avro::schema::Schema {
-                #implementation
+                #get_schema_impl
+            }
+
+            fn get_record_fields_in_ctxt(named_schemas: &mut 
apache_avro::schema::Names, enclosing_namespace: &Option<String>) -> 
Option<std::vec::Vec<apache_avro::schema::RecordField>> {
+                #get_record_fields_impl
             }
         }
     }
@@ -117,7 +135,7 @@ fn get_struct_schema_def(
     container_attrs: &NamedTypeOptions,
     data_struct: DataStruct,
     ident_span: Span,
-) -> Result<TokenStream, Vec<syn::Error>> {
+) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
     let mut record_field_exprs = vec![];
     match data_struct.fields {
         Fields::Named(a) => {
@@ -146,15 +164,13 @@ fn get_struct_schema_def(
                 } else if field_attrs.flatten {
                     // Inline the fields of the child record at runtime, as we 
don't have access to
                     // the schema here.
-                    let flatten_ty = &field.ty;
+                    let get_record_fields =
+                        get_field_get_record_fields_expr(&field, 
field_attrs.with)?;
                     record_field_exprs.push(quote! {
-                        if let 
::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema { 
fields, .. }) = #flatten_ty::get_schema() {
-                            for mut field in fields {
-                                field.position = schema_fields.len();
-                                schema_fields.push(field)
-                            }
+                        if let Some(flattened_fields) = #get_record_fields {
+                            schema_fields.extend(flattened_fields)
                         } else {
-                            panic!("Can only flatten RecordSchema, got {:?}", 
#flatten_ty::get_schema())
+                            panic!("#field does not have any fields to flatten 
to")
                         }
                     });
 
@@ -214,7 +230,7 @@ fn get_struct_schema_def(
     // the most common case where there is no flatten.
     let minimum_fields = record_field_exprs.len();
 
-    Ok(quote! {
+    let schema_def = quote! {
         {
             let mut schema_fields = Vec::with_capacity(#minimum_fields);
             #(#record_field_exprs)*
@@ -234,14 +250,21 @@ fn get_struct_schema_def(
                 attributes: Default::default(),
             })
         }
-    })
+    };
+    let record_fields = quote! {
+        let mut schema_fields = Vec::with_capacity(#minimum_fields);
+        #(#record_field_exprs)*
+        Some(schema_fields)
+    };
+
+    Ok((schema_def, record_fields))
 }
 
 /// Use the schema definition of the only field in the struct as the schema
 fn get_transparent_struct_schema_def(
     fields: Fields,
     input_span: Span,
-) -> Result<TokenStream, Vec<syn::Error>> {
+) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
     match fields {
         Fields::Named(fields_named) => {
             let mut found = None;
@@ -259,7 +282,10 @@ fn get_transparent_struct_schema_def(
             }
 
             if let Some((field, attrs)) = found {
-                get_field_schema_expr(&field, attrs.with)
+                Ok((
+                    get_field_schema_expr(&field, attrs.with.clone())?,
+                    get_field_get_record_fields_expr(&field, attrs.with)?,
+                ))
             } else {
                 Err(vec![syn::Error::new(
                     input_span,
@@ -302,6 +328,41 @@ fn get_field_schema_expr(field: &Field, with: With) -> 
Result<TokenStream, Vec<s
     }
 }
 
+fn get_field_get_record_fields_expr(
+    field: &Field,
+    with: With,
+) -> Result<TokenStream, Vec<syn::Error>> {
+    match with {
+        With::Trait => Ok(type_to_get_record_fields_expr(&field.ty)?),
+        With::Serde(path) => {
+            Ok(quote! { #path::get_record_fields_in_ctxt(named_schemas, 
enclosing_namespace) })
+        }
+        With::Expr(Expr::Closure(closure)) => {
+            if closure.inputs.is_empty() {
+                Ok(quote! {
+                    ::apache_avro::serde::get_record_fields_in_ctxt(
+                        named_schemas,
+                        enclosing_namespace,
+                        |_, _| (#closure)(),
+                    )
+                })
+            } else {
+                Err(vec![syn::Error::new(
+                    field.span(),
+                    "Expected closure with 0 parameters",
+                )])
+            }
+        }
+        With::Expr(Expr::Path(path)) => Ok(quote! {
+            ::apache_avro::serde::get_record_fields_in_ctxt(named_schemas, 
enclosing_namespace, #path)
+        }),
+        With::Expr(_expr) => Err(vec![syn::Error::new(
+            field.span(),
+            "Invalid expression, expected function or closure",
+        )]),
+    }
+}
+
 /// Generate a schema definition for a enum.
 fn get_data_enum_schema_def(
     container_attrs: &NamedTypeOptions,
@@ -367,6 +428,28 @@ fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, 
Vec<syn::Error>> {
     }
 }
 
+fn type_to_get_record_fields_expr(ty: &Type) -> Result<TokenStream, 
Vec<syn::Error>> {
+    match ty {
+        Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) 
=> Ok(
+            quote! {<#ty as 
apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt(named_schemas, 
enclosing_namespace)},
+        ),
+        Type::Ptr(_) => Err(vec![syn::Error::new_spanned(
+            ty,
+            "AvroSchema: derive does not support raw pointers",
+        )]),
+        Type::Tuple(_) => Err(vec![syn::Error::new_spanned(
+            ty,
+            "AvroSchema: derive does not support tuples",
+        )]),
+        _ => Err(vec![syn::Error::new_spanned(
+            ty,
+            format!(
+                "AvroSchema: Unexpected type encountered! Please open an issue 
if this kind of type should be supported: {ty:?}"
+            ),
+        )]),
+    }
+}
+
 fn default_enum_variant(
     data_enum: &syn::DataEnum,
     error_span: Span,
@@ -564,6 +647,13 @@ mod tests {
                                 schema
                             }
                         }
+
+                        fn get_record_fields_in_ctxt(
+                            named_schemas: &mut apache_avro::schema::Names,
+                            enclosing_namespace: &Option<String>
+                        ) -> Option 
<std::vec::Vec<apache_avro::schema::RecordField>> {
+                            None
+                        }
                     }
                 }.to_string());
             }
@@ -690,7 +780,7 @@ mod tests {
         match syn::parse2::<DeriveInput>(test_struct) {
             Ok(input) => {
                 let schema_res = derive_avro_schema(input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -709,7 +799,7 @@ mod tests {
         match syn::parse2::<DeriveInput>(test_enum) {
             Ok(input) => {
                 let schema_res = derive_avro_schema(input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -732,7 +822,7 @@ mod tests {
         match syn::parse2::<DeriveInput>(test_struct) {
             Ok(input) => {
                 let schema_res = derive_avro_schema(input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -752,7 +842,7 @@ mod tests {
         match syn::parse2::<DeriveInput>(test_enum) {
             Ok(input) => {
                 let schema_res = derive_avro_schema(input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for B { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("B") . expect (concat ! ("Unable to parse schema name " , "B")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for B { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("B") . expect (concat ! ("Unable to parse schema name " , "B")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -776,7 +866,7 @@ mod tests {
         match syn::parse2::<DeriveInput>(test_struct) {
             Ok(input) => {
                 let schema_res = derive_avro_schema(input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas 
: & mut apache_avro :: schema :: Names , enclosing_namespace : & Option < 
String >) -> apache_avro :: schema :: Schema { let name = apache_avro :: schema 
:: Name :: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) 
. fully_qualified_name (enclosing_namespace) ; if named_schemas . contains_key 
(& name) { apache_avr [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
diff --git a/avro_derive/tests/derive.rs b/avro_derive/tests/derive.rs
index 4ee27f0..6f04c8d 100644
--- a/avro_derive/tests/derive.rs
+++ b/avro_derive/tests/derive.rs
@@ -2155,3 +2155,65 @@ fn avro_rs_414_round_trip_char_u64_u128_i128() {
         d: i128::MAX,
     });
 }
+
+#[test]
+fn avro_rs_xxx_transparent_recurring_type() {
+    #[derive(AvroSchema)]
+    #[expect(dead_code, reason = "Only testing derived schema")]
+    pub enum Color {
+        G,
+    }
+
+    #[derive(AvroSchema)]
+    #[expect(dead_code, reason = "Only testing derived schema")]
+    pub struct A {
+        pub color: Color,
+    }
+
+    #[derive(AvroSchema)]
+    #[expect(dead_code, reason = "Only testing derived schema")]
+    pub struct C {
+        #[serde(flatten)]
+        pub a: A,
+    }
+
+    #[derive(AvroSchema)]
+    #[expect(dead_code, reason = "Only testing derived schema")]
+    pub struct TestStruct {
+        pub a: Color,
+        pub c: C,
+    }
+
+    let schema = Schema::parse_str(
+        r#"{
+        "name": "TestStruct",
+        "type":"record",
+        "fields": [
+            {
+                "name":"a",
+                "type": {
+                    "name": "Color",
+                    "type": "enum",
+                    "symbols": ["G"]
+                }
+            },
+            {
+                "name":"c",
+                "type": {
+                    "name":"C",
+                    "type":"record",
+                    "fields": [
+                        {
+                            "name": "color",
+                            "type": "Color"
+                        }
+                    ]
+                }
+            }
+        ]
+    }"#,
+    )
+    .unwrap();
+
+    assert_eq!(TestStruct::get_schema(), schema);
+}

Reply via email to