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

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


The following commit(s) were added to refs/heads/main by this push:
     new 7cff925  fix: `flatten` no longer causes duplicate names (#448)
7cff925 is described below

commit 7cff9252e1bbfcc53059199c48590f28a091df91
Author: Kriskras99 <[email protected]>
AuthorDate: Fri Jan 30 19:11:31 2026 +0100

    fix: `flatten` no longer causes duplicate names (#448)
    
    * 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.
    
    * feat: Use macros to reduce code duplication when implementing 
`AvroSchemaComponent`
    
    The implementation for `serde_json::Map<String, T>` has been removed. This 
is **not** a breaking change, as `serde_json::Map` can only be constructed for 
`<String, Value>` and `Value` does not implement `AvroSchemaComponent`.
    
    * fix: Add more tests
    
    * fix: Have flatten update field positions
    
    * feat: Update `AvroSchemaComponent::get_record_fields_in_ctxt` to also 
take the wanted field position
    
    * fix: Use `unwrap_or_else(|| panic!(...))` instead of `expect`
    
    * fix: Accidential rename and use more `::` in `quote!` macros
    
    ---------
    
    Co-authored-by: default <[email protected]>
---
 avro/src/serde/derive.rs          | 331 ++++++++++++++++++++++++++++++--------
 avro/src/serde/mod.rs             |   3 +
 avro/src/serde/with.rs            |  74 +++++++--
 avro/tests/get_record_fields.rs   | 149 +++++++++++++++++
 avro_derive/src/attributes/mod.rs |   2 +-
 avro_derive/src/lib.rs            | 157 ++++++++++++++----
 avro_derive/tests/derive.rs       | 216 +++++++++++++++++++++++++
 7 files changed, 819 insertions(+), 113 deletions(-)

diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs
index b51cee7..5164458 100644
--- a/avro/src/serde/derive.rs
+++ b/avro/src/serde/derive.rs
@@ -16,8 +16,9 @@
 // under the License.
 
 use crate::Schema;
-use crate::schema::{FixedSchema, Name, Names, Namespace, UnionSchema, 
UuidSchema};
-use serde_json::Map;
+use crate::schema::{
+    FixedSchema, Name, Names, Namespace, RecordField, RecordSchema, 
UnionSchema, UuidSchema,
+};
 use std::borrow::Cow;
 use std::collections::HashMap;
 
@@ -82,7 +83,160 @@ pub trait AvroSchema {
 ///}
 /// ```
 pub trait AvroSchemaComponent {
+    /// Get the schema for this component
     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(
+        first_field_position: usize,
+        named_schemas: &mut Names,
+        enclosing_namespace: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        get_record_fields_in_ctxt(
+            first_field_position,
+            named_schemas,
+            enclosing_namespace,
+            Self::get_schema_in_ctxt,
+        )
+    }
+}
+
+/// Get the record fields from `schema_fn` without polluting `named_schemas` 
or causing duplicate names
+///
+/// This is public so the derive macro can use it for `#[avro(with = ||)]` and 
`#[avro(with = path)]`
+pub fn get_record_fields_in_ctxt(
+    first_field_position: usize,
+    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)
+                .unwrap_or_else(|| panic!("Name '{name}' should exist in 
`named_schemas` otherwise Ref is invalid: {named_schemas:?}"));
+            // 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;
+            };
+            let fields = record
+                .fields
+                .into_iter()
+                .map(|mut f| {
+                    f.position += first_field_position;
+                    f
+                })
+                .collect();
+            return Some(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();
+
+    // Remove the name in case it is not used
+    named_schemas.remove(&record.name);
+
+    // 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 name = match std::mem::replace(schema, 
Schema::Record(new_schema)) {
+                Schema::Ref { name } => name,
+                schema => {
+                    panic!("Only expected `Schema::Ref` from `find_first_ref`, 
got: {schema:?}")
+                }
+            };
+
+            // The schema is used, so reinsert it
+            named_schemas.insert(name.clone(), Schema::Ref { name });
+
+            break;
+        }
+    }
+
+    let fields = record
+        .fields
+        .into_iter()
+        .map(|mut f| {
+            f.position += first_field_position;
+            f
+        })
+        .collect();
+    Some(fields)
 }
 
 impl<T> AvroSchema for T
@@ -100,6 +254,10 @@ macro_rules! impl_schema (
             fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
                 $variant_constructor
             }
+
+            fn get_record_fields_in_ctxt(_: usize, _: &mut Names, _: 
&Namespace) -> Option<Vec<RecordField>> {
+                None
+            }
         }
     );
 );
@@ -118,32 +276,44 @@ impl_schema!(String, Schema::String);
 impl_schema!(str, Schema::String);
 impl_schema!(char, Schema::String);
 
-impl<T> AvroSchemaComponent for &T
-where
-    T: AvroSchemaComponent + ?Sized,
-{
-    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
-        T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
-    }
-}
+macro_rules! impl_passthrough_schema (
+    ($type:ty where T: AvroSchemaComponent + ?Sized $(+ $bound:tt)*) => (
+        impl<T: AvroSchemaComponent $(+ $bound)* + ?Sized> AvroSchemaComponent 
for $type {
+            fn get_schema_in_ctxt(named_schemas: &mut Names, 
enclosing_namespace: &Namespace) -> Schema {
+                T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
+            }
 
-impl<T> AvroSchemaComponent for &mut T
-where
-    T: AvroSchemaComponent + ?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(first_field_position: usize, 
named_schemas: &mut Names, enclosing_namespace: &Namespace) -> 
Option<Vec<RecordField>> {
+                T::get_record_fields_in_ctxt(first_field_position, 
named_schemas, enclosing_namespace)
+            }
+        }
+    );
+);
 
-impl<T> AvroSchemaComponent for [T]
-where
-    T: AvroSchemaComponent,
-{
-    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
-        Schema::array(T::get_schema_in_ctxt(named_schemas, 
enclosing_namespace))
-    }
-}
+impl_passthrough_schema!(&T where T: AvroSchemaComponent + ?Sized);
+impl_passthrough_schema!(&mut T where T: AvroSchemaComponent + ?Sized);
+impl_passthrough_schema!(Box<T> where T: AvroSchemaComponent + ?Sized);
+impl_passthrough_schema!(Cow<'_, T> where T: AvroSchemaComponent + ?Sized + 
ToOwned);
+impl_passthrough_schema!(std::sync::Mutex<T> where T: AvroSchemaComponent + 
?Sized);
+
+macro_rules! impl_array_schema (
+    ($type:ty where T: AvroSchemaComponent) => (
+        impl<T: AvroSchemaComponent> AvroSchemaComponent for $type {
+            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(_: usize, _: &mut Names, _: 
&Namespace) -> Option<Vec<RecordField>> {
+                None
+            }
+        }
+    );
+);
+
+impl_array_schema!([T] where T: AvroSchemaComponent);
+impl_array_schema!(Vec<T> where T: AvroSchemaComponent);
+// This doesn't work as the macro doesn't allow specifying the N parameter
+// impl_array_schema!([T; N] where T: AvroSchemaComponent);
 
 impl<const N: usize, T> AvroSchemaComponent for [T; N]
 where
@@ -152,14 +322,30 @@ 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(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
 }
 
-impl<T> AvroSchemaComponent for Vec<T>
+impl<T> AvroSchemaComponent for HashMap<String, T>
 where
     T: AvroSchemaComponent,
 {
     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
-        Schema::array(T::get_schema_in_ctxt(named_schemas, 
enclosing_namespace))
+        Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
+    }
+
+    fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
     }
 }
 
@@ -177,50 +363,13 @@ where
             UnionSchema::new(variants).expect("Option<T> must produce a valid 
(non-nested) union"),
         )
     }
-}
-
-impl<T> AvroSchemaComponent for Map<String, T>
-where
-    T: AvroSchemaComponent,
-{
-    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
-        Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
-    }
-}
 
-impl<T> AvroSchemaComponent for HashMap<String, T>
-where
-    T: AvroSchemaComponent,
-{
-    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
-        Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
-    }
-}
-
-impl<T> AvroSchemaComponent for Box<T>
-where
-    T: AvroSchemaComponent,
-{
-    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
-        T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
-    }
-}
-
-impl<T> AvroSchemaComponent for std::sync::Mutex<T>
-where
-    T: AvroSchemaComponent,
-{
-    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: 
&Namespace) -> Schema {
-        T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
-    }
-}
-
-impl<T> AvroSchemaComponent for Cow<'_, T>
-where
-    T: AvroSchemaComponent + Clone,
-{
-    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(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
     }
 }
 
@@ -248,6 +397,14 @@ impl AvroSchemaComponent for core::time::Duration {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl AvroSchemaComponent for uuid::Uuid {
@@ -274,6 +431,14 @@ impl AvroSchemaComponent for uuid::Uuid {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl AvroSchemaComponent for u64 {
@@ -298,6 +463,14 @@ impl AvroSchemaComponent for u64 {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl AvroSchemaComponent for u128 {
@@ -322,6 +495,14 @@ impl AvroSchemaComponent for u128 {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
 }
 
 impl AvroSchemaComponent for i128 {
@@ -346,6 +527,14 @@ impl AvroSchemaComponent for i128 {
             schema
         }
     }
+
+    fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &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/src/serde/with.rs b/avro/src/serde/with.rs
index 70dc84a..670955b 100644
--- a/avro/src/serde/with.rs
+++ b/avro/src/serde/with.rs
@@ -95,14 +95,23 @@ pub mod bytes {
 
     use crate::{
         Schema,
-        schema::{Names, Namespace},
+        schema::{Names, Namespace, RecordField},
     };
 
     /// Returns [`Schema::Bytes`]
-    pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace: 
&Namespace) -> Schema {
+    pub fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
         Schema::Bytes
     }
 
+    /// Returns `None`
+    pub fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
+
     pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
@@ -147,16 +156,25 @@ pub mod bytes_opt {
 
     use crate::{
         Schema,
-        schema::{Names, Namespace, UnionSchema},
+        schema::{Names, Namespace, RecordField, UnionSchema},
     };
 
     /// Returns `Schema::Union(Schema::Null, Schema::Bytes)`
-    pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace: 
&Namespace) -> Schema {
+    pub fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
         Schema::Union(
             UnionSchema::new(vec![Schema::Null, Schema::Bytes]).expect("This 
is a valid union"),
         )
     }
 
+    /// Returns `None`
+    pub fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
+
     pub fn serialize<S, B>(bytes: &Option<B>, serializer: S) -> Result<S::Ok, 
S::Error>
     where
         S: Serializer,
@@ -202,7 +220,7 @@ pub mod fixed {
 
     use crate::{
         Schema,
-        schema::{FixedSchema, Name, Names, Namespace},
+        schema::{FixedSchema, Name, Names, Namespace, RecordField},
     };
 
     /// Returns `Schema::Fixed(N)` named `serde_avro_fixed_{N}`
@@ -223,6 +241,15 @@ pub mod fixed {
         }
     }
 
+    /// Returns `None`
+    pub fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
+
     pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
@@ -269,7 +296,7 @@ pub mod fixed_opt {
 
     use crate::{
         Schema,
-        schema::{Names, Namespace, UnionSchema},
+        schema::{Names, Namespace, RecordField, UnionSchema},
     };
 
     /// Returns `Schema::Union(Schema::Null, Schema::Fixed(N))` where the 
fixed schema is named `serde_avro_fixed_{N}`
@@ -286,6 +313,15 @@ pub mod fixed_opt {
         )
     }
 
+    /// Returns `None`
+    pub fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
+
     pub fn serialize<S, B>(bytes: &Option<B>, serializer: S) -> Result<S::Ok, 
S::Error>
     where
         S: Serializer,
@@ -333,14 +369,23 @@ pub mod slice {
 
     use crate::{
         Schema,
-        schema::{Names, Namespace},
+        schema::{Names, Namespace, RecordField},
     };
 
     /// Returns [`Schema::Bytes`]
-    pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace: 
&Namespace) -> Schema {
+    pub fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
         Schema::Bytes
     }
 
+    /// Returns `None`
+    pub fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
+
     pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
@@ -388,16 +433,25 @@ pub mod slice_opt {
 
     use crate::{
         Schema,
-        schema::{Names, Namespace, UnionSchema},
+        schema::{Names, Namespace, RecordField, UnionSchema},
     };
 
     /// Returns `Schema::Union(Schema::Null, Schema::Bytes)`
-    pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace: 
&Namespace) -> Schema {
+    pub fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
         Schema::Union(
             UnionSchema::new(vec![Schema::Null, Schema::Bytes]).expect("This 
is a valid union"),
         )
     }
 
+    /// Returns `None`
+    pub fn get_record_fields_in_ctxt(
+        _: usize,
+        _: &mut Names,
+        _: &Namespace,
+    ) -> Option<Vec<RecordField>> {
+        None
+    }
+
     pub fn serialize<S, B>(bytes: &Option<B>, serializer: S) -> Result<S::Ok, 
S::Error>
     where
         S: Serializer,
diff --git a/avro/tests/get_record_fields.rs b/avro/tests/get_record_fields.rs
new file mode 100644
index 0000000..9b729fe
--- /dev/null
+++ b/avro/tests/get_record_fields.rs
@@ -0,0 +1,149 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use apache_avro::{
+    Schema,
+    serde::{AvroSchemaComponent, get_record_fields_in_ctxt},
+};
+use std::collections::HashMap;
+
+use apache_avro_test_helper::TestResult;
+
+#[test]
+fn avro_rs_448_default_get_record_fields_no_recursion() -> TestResult {
+    #[derive(apache_avro_derive::AvroSchema)]
+    struct Foo {
+        _a: i32,
+        _b: String,
+    }
+
+    let mut named_schemas = HashMap::new();
+    let fields =
+        get_record_fields_in_ctxt(0, &mut named_schemas, &None, 
Foo::get_schema_in_ctxt).unwrap();
+
+    assert_eq!(fields.len(), 2);
+    assert!(
+        named_schemas.is_empty(),
+        "Name shouldn't have been added: {named_schemas:?}"
+    );
+
+    // Insert Foo into named_schemas
+    match Foo::get_schema_in_ctxt(&mut named_schemas, &None) {
+        Schema::Record(_) => {}
+        schema => panic!("Expected a record got {schema:?}"),
+    }
+    assert_eq!(
+        named_schemas.len(),
+        1,
+        "Name should have been added: {named_schemas:?}"
+    );
+
+    let fields =
+        get_record_fields_in_ctxt(0, &mut named_schemas, &None, 
Foo::get_schema_in_ctxt).unwrap();
+    assert_eq!(fields.len(), 2);
+    assert_eq!(
+        named_schemas.len(),
+        1,
+        "Name shouldn't have been removed: {named_schemas:?}"
+    );
+
+    Ok(())
+}
+
+#[test]
+fn avro_rs_448_default_get_record_fields_recursion() -> TestResult {
+    #[derive(apache_avro_derive::AvroSchema)]
+    struct Foo {
+        _a: i32,
+        _b: Option<Box<Foo>>,
+    }
+
+    let mut named_schemas = HashMap::new();
+    let fields =
+        get_record_fields_in_ctxt(0, &mut named_schemas, &None, 
Foo::get_schema_in_ctxt).unwrap();
+
+    assert_eq!(fields.len(), 2);
+    assert_eq!(
+        named_schemas.len(),
+        1,
+        "Name shouldn't have been removed: {named_schemas:?}"
+    );
+
+    // Insert Foo into named_schemas
+    match Foo::get_schema_in_ctxt(&mut named_schemas, &None) {
+        Schema::Ref { name: _ } => {}
+        schema => panic!("Expected a ref got {schema:?}"),
+    }
+    assert_eq!(named_schemas.len(), 1);
+
+    let fields =
+        get_record_fields_in_ctxt(0, &mut named_schemas, &None, 
Foo::get_schema_in_ctxt).unwrap();
+    assert_eq!(fields.len(), 2);
+    assert_eq!(
+        named_schemas.len(),
+        1,
+        "Name shouldn't have been removed: {named_schemas:?}"
+    );
+
+    Ok(())
+}
+
+#[test]
+fn avro_rs_448_default_get_record_fields_position() -> TestResult {
+    #[derive(apache_avro_derive::AvroSchema)]
+    struct Foo {
+        _a: i32,
+        _b: String,
+    }
+
+    let mut named_schemas = HashMap::new();
+    let fields =
+        get_record_fields_in_ctxt(10, &mut named_schemas, &None, 
Foo::get_schema_in_ctxt).unwrap();
+
+    assert_eq!(fields.len(), 2);
+    assert!(
+        named_schemas.is_empty(),
+        "Name shouldn't have been added: {named_schemas:?}"
+    );
+    let positions = fields.into_iter().map(|f| f.position).collect::<Vec<_>>();
+    assert_eq!(positions.as_slice(), &[10, 11][..]);
+
+    // Insert Foo into named_schemas
+    match Foo::get_schema_in_ctxt(&mut named_schemas, &None) {
+        Schema::Record(_) => {}
+        schema => panic!("Expected a record got {schema:?}"),
+    }
+    assert_eq!(
+        named_schemas.len(),
+        1,
+        "Name should have been added: {named_schemas:?}"
+    );
+
+    let fields =
+        get_record_fields_in_ctxt(5043, &mut named_schemas, &None, 
Foo::get_schema_in_ctxt)
+            .unwrap();
+    assert_eq!(fields.len(), 2);
+    assert_eq!(
+        named_schemas.len(),
+        1,
+        "Name shouldn't have been removed: {named_schemas:?}"
+    );
+    let positions = fields.into_iter().map(|f| f.position).collect::<Vec<_>>();
+    assert_eq!(positions.as_slice(), &[5043, 5044][..]);
+
+    Ok(())
+}
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..3904452 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
+        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: 
&::std::option::Option<::std::string::String>) -> ::apache_avro::schema::Schema 
{
+                #get_schema_impl
+            }
+
+            fn get_record_fields_in_ctxt(mut field_position: usize, 
named_schemas: &mut ::apache_avro::schema::Names, enclosing_namespace: 
&::std::option::Option<::std::string::String>) -> 
::std::option::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,14 @@ 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 {
+                            field_position += flattened_fields.len();
+                            schema_fields.extend(flattened_fields);
                         } else {
-                            panic!("Can only flatten RecordSchema, got {:?}", 
#flatten_ty::get_schema())
+                            panic!("{} does not have any fields to flatten 
to", stringify!(#field));
                         }
                     });
 
@@ -186,9 +203,10 @@ fn get_struct_schema_def(
                         aliases: #aliases,
                         schema: #schema_expr,
                         order: 
::apache_avro::schema::RecordFieldOrder::Ascending,
-                        position: schema_fields.len(),
+                        position: field_position,
                         custom_attributes: Default::default(),
                     });
+                    field_position += 1;
                 });
             }
         }
@@ -214,9 +232,10 @@ 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);
+            let mut field_position = 0;
             #(#record_field_exprs)*
             let schema_field_set: ::std::collections::HashSet<_> = 
schema_fields.iter().map(|rf| &rf.name).collect();
             assert_eq!(schema_fields.len(), schema_field_set.len(), "Duplicate 
field names found: {schema_fields:?}");
@@ -234,14 +253,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 +285,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 +331,42 @@ 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(field_position, 
named_schemas, enclosing_namespace) },
+        ),
+        With::Expr(Expr::Closure(closure)) => {
+            if closure.inputs.is_empty() {
+                Ok(quote! {
+                    ::apache_avro::serde::get_record_fields_in_ctxt(
+                        field_position,
+                        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(field_position, 
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 +432,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(field_position, 
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,
@@ -531,11 +618,11 @@ mod tests {
                 assert!(derived.is_ok());
                 assert_eq!(derived.unwrap().to_string(), quote! {
                     #[automatically_derived]
-                    impl apache_avro::AvroSchemaComponent for Basic {
+                    impl ::apache_avro::AvroSchemaComponent for Basic {
                         fn get_schema_in_ctxt(
-                            named_schemas: &mut apache_avro::schema::Names,
-                            enclosing_namespace: &Option<String>
-                        ) -> apache_avro::schema::Schema {
+                            named_schemas: &mut ::apache_avro::schema::Names,
+                            enclosing_namespace: 
&::std::option::Option<::std::string::String>
+                        ) -> ::apache_avro::schema::Schema {
                             let name = apache_avro::schema::Name::new("Basic")
                                 .expect(concat!("Unable to parse schema name 
", "Basic"))
                                 .fully_qualified_name(enclosing_namespace);
@@ -564,6 +651,14 @@ mod tests {
                                 schema
                             }
                         }
+
+                        fn get_record_fields_in_ctxt(
+                            mut field_position: usize,
+                            named_schemas: &mut ::apache_avro::schema::Names,
+                            enclosing_namespace: 
&::std::option::Option<::std::string::String>
+                        ) -> ::std::option::Option 
<::std::vec::Vec<::apache_avro::schema::RecordField>> {
+                            None
+                        }
                     }
                 }.to_string());
             }
@@ -690,7 +785,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 
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -709,7 +804,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 
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -732,7 +827,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 
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -752,7 +847,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 
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -776,7 +871,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 
: & :: std :: option :: Option < :: std :: string :: 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 n [...]
                 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..d6b1c4e 100644
--- a/avro_derive/tests/derive.rs
+++ b/avro_derive/tests/derive.rs
@@ -2155,3 +2155,219 @@ fn avro_rs_414_round_trip_char_u64_u128_i128() {
         d: i128::MAX,
     });
 }
+
+#[test]
+fn avro_rs_448_flatten_recurring_type() {
+    #[derive(AvroSchema)]
+    #[expect(dead_code, reason = "Only testing derived schema")]
+    pub enum Color {
+        G,
+    }
+
+    #[derive(AvroSchema)]
+    pub struct A {
+        pub _color: Color,
+    }
+
+    #[derive(AvroSchema)]
+    pub struct C {
+        #[serde(flatten)]
+        pub _a: A,
+    }
+
+    #[derive(AvroSchema)]
+    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);
+}
+
+#[test]
+fn avro_rs_448_flatten_transparent_sandwich() {
+    #[derive(AvroSchema)]
+    #[expect(dead_code, reason = "Only testing derived schema")]
+    pub enum Color {
+        G,
+    }
+
+    #[derive(AvroSchema)]
+    pub struct A {
+        pub _color: Color,
+    }
+
+    #[derive(AvroSchema)]
+    pub struct C {
+        #[serde(flatten)]
+        pub _a: A,
+    }
+
+    #[derive(AvroSchema)]
+    #[serde(transparent)]
+    pub struct B {
+        pub _c: C,
+    }
+
+    #[derive(AvroSchema)]
+    pub struct TestStruct {
+        pub _a: Color,
+        pub _b: B,
+        pub _c: C,
+    }
+
+    let schema = Schema::parse_str(
+        r#"{
+        "name": "TestStruct",
+        "type":"record",
+        "fields": [
+            {
+                "name":"_a",
+                "type": {
+                    "name": "Color",
+                    "type": "enum",
+                    "symbols": ["G"]
+                }
+            },
+            {
+                "name":"_b",
+                "type": {
+                    "name":"C",
+                    "type":"record",
+                    "fields": [
+                        {
+                            "name": "_color",
+                            "type": "Color"
+                        }
+                    ]
+                }
+            },
+            {
+                "name":"_c",
+                "type": "C"
+            }
+        ]
+    }"#,
+    )
+    .unwrap();
+
+    assert_eq!(TestStruct::get_schema(), schema);
+}
+
+#[test]
+fn avro_rs_448_transparent_with() {
+    #[derive(AvroSchema)]
+    #[serde(transparent)]
+    pub struct TestStruct {
+        #[avro(with = || Schema::Long)]
+        pub _a: i32,
+    }
+
+    let mut named_schemas = HashMap::new();
+    assert_eq!(
+        TestStruct::get_record_fields_in_ctxt(0, &mut named_schemas, &None),
+        None
+    );
+    assert!(
+        named_schemas.is_empty(),
+        "No name should've been added: {named_schemas:?}"
+    );
+}
+
+#[test]
+fn avro_rs_448_transparent_with_2() {
+    #[derive(AvroSchema)]
+    pub struct Foo {
+        _field: i32,
+        _a: String,
+    }
+
+    #[derive(AvroSchema)]
+    #[serde(transparent)]
+    pub struct TestStruct {
+        #[avro(with = Foo::get_schema_in_ctxt)]
+        pub _a: Foo,
+    }
+
+    let mut named_schemas = HashMap::new();
+    let fields = TestStruct::get_record_fields_in_ctxt(0, &mut named_schemas, 
&None).unwrap();
+    assert!(
+        named_schemas.is_empty(),
+        "No name should've been added: {named_schemas:?}"
+    );
+    assert_eq!(fields.len(), 2);
+
+    TestStruct::get_schema_in_ctxt(&mut named_schemas, &None);
+    assert_eq!(
+        named_schemas.len(),
+        1,
+        "One name should've been added: {named_schemas:?}"
+    );
+
+    let fields = TestStruct::get_record_fields_in_ctxt(0, &mut named_schemas, 
&None).unwrap();
+    assert_eq!(
+        named_schemas.len(),
+        1,
+        "No name should've been added: {named_schemas:?}"
+    );
+    assert_eq!(fields.len(), 2);
+}
+
+#[test]
+fn avro_rs_448_flatten_field_positions() {
+    #[derive(AvroSchema)]
+    struct Foo {
+        _a: i32,
+        _b: String,
+    }
+
+    #[derive(AvroSchema)]
+    struct Bar {
+        _c: Vec<i64>,
+        #[serde(flatten)]
+        _d: Foo,
+        _e: bool,
+    }
+
+    let Schema::Record(schema) = Bar::get_schema() else {
+        panic!("Structs should generate records")
+    };
+
+    let positions = schema
+        .fields
+        .into_iter()
+        .map(|f| f.position)
+        .collect::<Vec<_>>();
+    assert_eq!(positions.as_slice(), &[0, 1, 2, 3][..]);
+}

Reply via email to