You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@avro.apache.org by kr...@apache.org on 2026/06/20 08:51:02 UTC

(avro-rs) branch refactor/derive created (now 829daf0)

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

Kriskras99 pushed a change to branch refactor/derive
in repository https://gitbox.apache.org/repos/asf/avro-rs.git


      at 829daf0  refactor: Improve code structure of the derive code

This branch includes the following new commits:

     new 829daf0  refactor: Improve code structure of the derive code

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(avro-rs) 01/01: refactor: Improve code structure of the derive code

Posted by kr...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 829daf0a5211a6933a04c2994ea9f8465bcc2878
Author: Kriskras99 <gi...@kriskras99.nl>
AuthorDate: Sat Jun 20 10:50:00 2026 +0200

    refactor: Improve code structure of the derive code
    
    This also changes the implementation of `utils::field_aliases` to remove
    the unnecessary `try_from`.
---
 avro_derive/src/attributes/serde.rs                |   2 +-
 avro_derive/src/fields.rs                          | 165 +++++++++
 avro_derive/src/lib.rs                             | 368 +--------------------
 avro_derive/src/structs.rs                         | 169 ++++++++++
 avro_derive/src/utils.rs                           |  50 +++
 .../avro_3709_record_field_attributes.expanded.rs  |  10 +-
 .../avro_rs_207_rename_all_attribute.expanded.rs   |   8 +-
 ...name_attr_over_rename_all_attribute.expanded.rs |   8 +-
 .../tests/expanded/avro_rs_501_basic.expanded.rs   |   8 +-
 .../expanded/avro_rs_501_namespace.expanded.rs     |   8 +-
 .../expanded/avro_rs_501_reference.expanded.rs     |   8 +-
 .../avro_rs_501_struct_with_optional.expanded.rs   |   4 +-
 12 files changed, 417 insertions(+), 391 deletions(-)

diff --git a/avro_derive/src/attributes/serde.rs b/avro_derive/src/attributes/serde.rs
index b1ca6fa..83d0711 100644
--- a/avro_derive/src/attributes/serde.rs
+++ b/avro_derive/src/attributes/serde.rs
@@ -249,8 +249,8 @@ pub struct FieldAttributes {
 #[cfg(test)]
 mod tests {
     use crate::{
-        RenameRule,
         attributes::serde::{ContainerAttributes, RenameAll, SerdeDefault},
+        case::RenameRule,
     };
     use darling::FromAttributes;
     use syn::DeriveInput;
diff --git a/avro_derive/src/fields.rs b/avro_derive/src/fields.rs
new file mode 100644
index 0000000..c8024a4
--- /dev/null
+++ b/avro_derive/src/fields.rs
@@ -0,0 +1,165 @@
+use crate::attributes::{FieldDefault, With};
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{Expr, Field, Type};
+
+pub fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, Vec<syn::Error>> {
+    match with {
+        With::Trait => Ok(type_to_schema_expr(&field.ty)?),
+        With::Serde(path) => {
+            Ok(quote! { #path::get_schema_in_ctxt(named_schemas, enclosing_namespace) })
+        }
+        With::Expr(Expr::Closure(closure)) => {
+            if closure.inputs.is_empty() {
+                Ok(quote! { (#closure)() })
+            } else {
+                Err(vec![syn::Error::new(
+                    field.span(),
+                    "Expected closure with 0 parameters",
+                )])
+            }
+        }
+        With::Expr(Expr::Path(path)) => Ok(quote! { #path(named_schemas, enclosing_namespace) }),
+        With::Expr(_expr) => Err(vec![syn::Error::new(
+            field.span(),
+            "Invalid expression, expected function or closure",
+        )]),
+    }
+}
+
+pub 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",
+        )]),
+    }
+}
+
+pub fn get_field_field_default_expr(
+    field: &Field,
+    default: FieldDefault,
+) -> Result<TokenStream, Vec<syn::Error>> {
+    match default {
+        FieldDefault::Disabled => Ok(quote! { ::std::option::Option::None }),
+        FieldDefault::Trait => type_to_field_default_expr(&field.ty),
+        FieldDefault::Value(default_value) => {
+            let _: serde_json::Value = serde_json::from_str(&default_value[..]).map_err(|e| {
+                vec![syn::Error::new(
+                    field.ident.span(),
+                    format!("Invalid avro default json: \n{e}"),
+                )]
+            })?;
+            Ok(quote! {
+                ::std::option::Option::Some(::serde_json::from_str(#default_value).expect("Unreachable! This parsed at compile time!"))
+            })
+        }
+    }
+}
+
+/// 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>> {
+    match ty {
+        Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok(
+            quote! {<#ty as :: apache_avro::AvroSchemaComponent>::get_schema_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 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 type_to_field_default_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>::field_default()})
+        }
+        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:?}"
+            ),
+        )]),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn test_trait_cast() {
+        assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{i32}).unwrap()).unwrap().to_string(), quote!{<i32 as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
+        assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{Vec<T>}).unwrap()).unwrap().to_string(), quote!{<Vec<T> as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
+        assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{AnyType}).unwrap()).unwrap().to_string(), quote!{<AnyType as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
+    }
+}
diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs
index d63ed8f..e2bc329 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -32,19 +32,18 @@
 mod attributes;
 mod case;
 mod enums;
+mod fields;
+mod structs;
+mod utils;
 
-use proc_macro2::{Span, TokenStream};
+use proc_macro2::TokenStream;
 use quote::quote;
-use syn::{
-    DataStruct, DeriveInput, Expr, Field, Fields, Generics, Ident, Type, parse_macro_input,
-    spanned::Spanned,
-};
+use syn::{DeriveInput, Generics, Ident, parse_macro_input, spanned::Spanned};
 
+use crate::attributes::NamedTypeOptions;
 use crate::enums::get_data_enum_schema_def;
-use crate::{
-    attributes::{FieldDefault, FieldOptions, NamedTypeOptions, With},
-    case::RenameRule,
-};
+use crate::structs::{get_struct_schema_def, get_transparent_struct_schema_def};
+use crate::utils::{aliases, preserve_optional, to_compile_errors};
 
 #[proc_macro_derive(AvroSchema, attributes(avro, serde))]
 // Templated from Serde
@@ -146,354 +145,3 @@ fn handle_named_schemas(full_schema_name: &str, schema_def: &TokenStream) -> Tok
         }
     }
 }
-
-/// Generate a schema definition for a struct.
-fn get_struct_schema_def(
-    container_attrs: &NamedTypeOptions,
-    data_struct: DataStruct,
-    ident_span: Span,
-) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
-    let mut record_field_exprs = vec![];
-    match data_struct.fields {
-        Fields::Named(a) => {
-            for field in a.named {
-                let mut name = field
-                    .ident
-                    .as_ref()
-                    .expect("Field must have a name")
-                    .to_string();
-                if let Some(raw_name) = name.strip_prefix("r#") {
-                    name = raw_name.to_string();
-                }
-                let field_attrs = FieldOptions::new(&field.attrs, field.span())?;
-                let doc = preserve_optional(field_attrs.doc);
-                match (field_attrs.rename, container_attrs.rename_all) {
-                    (Some(rename), _) => {
-                        name = rename;
-                    }
-                    (None, rename_all) if rename_all != RenameRule::None => {
-                        name = rename_all.apply_to_field(&name);
-                    }
-                    _ => {}
-                }
-                if field_attrs.skip {
-                    continue;
-                } 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 get_record_fields =
-                        get_field_get_record_fields_expr(&field, field_attrs.with)?;
-                    record_field_exprs.push(quote! {
-                        if let Some(flattened_fields) = #get_record_fields {
-                            schema_fields.extend(flattened_fields);
-                        } else {
-                            panic!("{} does not have any fields to flatten to", stringify!(#field));
-                        }
-                    });
-
-                    // Don't add this field as it's been replaced by the child record fields
-                    continue;
-                }
-                let default_value = match field_attrs.default {
-                    FieldDefault::Disabled => quote! { ::std::option::Option::None },
-                    FieldDefault::Trait => type_to_field_default_expr(&field.ty)?,
-                    FieldDefault::Value(default_value) => {
-                        let _: serde_json::Value = serde_json::from_str(&default_value[..])
-                            .map_err(|e| {
-                                vec![syn::Error::new(
-                                    field.ident.span(),
-                                    format!("Invalid avro default json: \n{e}"),
-                                )]
-                            })?;
-                        quote! {
-                            ::std::option::Option::Some(::serde_json::from_str(#default_value).expect("Unreachable! This parsed at compile time!"))
-                        }
-                    }
-                };
-                let aliases = field_aliases(&field_attrs.alias);
-                let schema_expr = get_field_schema_expr(&field, field_attrs.with)?;
-                record_field_exprs.push(quote! {
-                    schema_fields.push(::apache_avro::schema::RecordField {
-                        name: #name.to_string(),
-                        doc: #doc,
-                        default: #default_value,
-                        aliases: #aliases,
-                        schema: #schema_expr,
-                        custom_attributes: ::std::collections::BTreeMap::new(),
-                    });
-                });
-            }
-        }
-        Fields::Unnamed(_) => {
-            return Err(vec![syn::Error::new(
-                ident_span,
-                "AvroSchema derive does not work for tuple structs",
-            )]);
-        }
-        Fields::Unit => {
-            return Err(vec![syn::Error::new(
-                ident_span,
-                "AvroSchema derive does not work for unit structs",
-            )]);
-        }
-    }
-
-    let record_doc = preserve_optional(container_attrs.doc.as_ref());
-    let record_aliases = aliases(&container_attrs.aliases);
-    let full_schema_name = &container_attrs.name;
-
-    // When flatten is involved, there will be more but we don't know how many. This optimises for
-    // the most common case where there is no flatten.
-    let minimum_fields = record_field_exprs.len();
-
-    let schema_def = quote! {
-        {
-            let mut schema_fields = ::std::vec::Vec::with_capacity(#minimum_fields);
-            #(#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:?}");
-            let name = ::apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to parse struct name for schema {}", #full_schema_name)[..]);
-            let lookup: ::std::collections::BTreeMap<String, usize> = schema_fields
-                .iter()
-                .enumerate()
-                .map(|(position, field)| (field.name.to_owned(), position))
-                .collect();
-            ::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema {
-                name,
-                aliases: #record_aliases,
-                doc: #record_doc,
-                fields: schema_fields,
-                lookup,
-                attributes: ::std::collections::BTreeMap::new(),
-            })
-        }
-    };
-    let record_fields = quote! {
-        let mut schema_fields = ::std::vec::Vec::with_capacity(#minimum_fields);
-        #(#record_field_exprs)*
-        ::std::option::Option::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, TokenStream), Vec<syn::Error>> {
-    match fields {
-        Fields::Named(fields_named) => {
-            let mut found = None;
-            for field in fields_named.named {
-                let attrs = FieldOptions::new(&field.attrs, field.span())?;
-                if attrs.skip {
-                    continue;
-                }
-                if found.replace((field, attrs)).is_some() {
-                    return Err(vec![syn::Error::new(
-                        input_span,
-                        "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field",
-                    )]);
-                }
-            }
-
-            if let Some((field, attrs)) = found {
-                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,
-                    "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field",
-                )])
-            }
-        }
-        Fields::Unnamed(_) => Err(vec![syn::Error::new(
-            input_span,
-            "AvroSchema: derive does not work for tuple structs",
-        )]),
-        Fields::Unit => Err(vec![syn::Error::new(
-            input_span,
-            "AvroSchema: derive does not work for unit structs",
-        )]),
-    }
-}
-
-fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, Vec<syn::Error>> {
-    match with {
-        With::Trait => Ok(type_to_schema_expr(&field.ty)?),
-        With::Serde(path) => {
-            Ok(quote! { #path::get_schema_in_ctxt(named_schemas, enclosing_namespace) })
-        }
-        With::Expr(Expr::Closure(closure)) => {
-            if closure.inputs.is_empty() {
-                Ok(quote! { (#closure)() })
-            } else {
-                Err(vec![syn::Error::new(
-                    field.span(),
-                    "Expected closure with 0 parameters",
-                )])
-            }
-        }
-        With::Expr(Expr::Path(path)) => Ok(quote! { #path(named_schemas, enclosing_namespace) }),
-        With::Expr(_expr) => Err(vec![syn::Error::new(
-            field.span(),
-            "Invalid expression, expected function or closure",
-        )]),
-    }
-}
-
-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",
-        )]),
-    }
-}
-
-/// 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>> {
-    match ty {
-        Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok(
-            quote! {<#ty as :: apache_avro::AvroSchemaComponent>::get_schema_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 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 type_to_field_default_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>::field_default()})
-        }
-        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:?}"
-            ),
-        )]),
-    }
-}
-
-/// Stolen from serde
-fn to_compile_errors(errors: impl AsRef<[syn::Error]>) -> proc_macro2::TokenStream {
-    let compile_errors = errors.as_ref().iter().map(syn::Error::to_compile_error);
-    quote!(#(#compile_errors)*)
-}
-
-fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream {
-    if let Some(tt) = op {
-        quote! {::std::option::Option::Some(#tt.into())}
-    } else {
-        quote! {::std::option::Option::None}
-    }
-}
-
-fn aliases(op: &[impl quote::ToTokens]) -> TokenStream {
-    let items: Vec<TokenStream> = op
-        .iter()
-        .map(|tt| quote! {#tt.try_into().expect("Alias is invalid")})
-        .collect();
-    if items.is_empty() {
-        quote! {::std::option::Option::None}
-    } else {
-        quote! {::std::option::Option::Some(vec![#(#items),*])}
-    }
-}
-
-fn field_aliases(op: &[impl quote::ToTokens]) -> TokenStream {
-    let items: Vec<TokenStream> = op
-        .iter()
-        .map(|tt| quote! {#tt.try_into().expect("Alias is invalid")})
-        .collect();
-    if items.is_empty() {
-        quote! {::std::vec::Vec::new()}
-    } else {
-        quote! {vec![#(#items),*]}
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use pretty_assertions::assert_eq;
-
-    #[test]
-    fn test_trait_cast() {
-        assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{i32}).unwrap()).unwrap().to_string(), quote!{<i32 as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
-        assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{Vec<T>}).unwrap()).unwrap().to_string(), quote!{<Vec<T> as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
-        assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{AnyType}).unwrap()).unwrap().to_string(), quote!{<AnyType as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string());
-    }
-}
diff --git a/avro_derive/src/structs.rs b/avro_derive/src/structs.rs
new file mode 100644
index 0000000..3ab84ae
--- /dev/null
+++ b/avro_derive/src/structs.rs
@@ -0,0 +1,169 @@
+use crate::attributes::{FieldOptions, NamedTypeOptions};
+use crate::case::RenameRule;
+use crate::fields::{
+    get_field_field_default_expr, get_field_get_record_fields_expr, get_field_schema_expr,
+};
+use crate::utils::{aliases, field_aliases, preserve_optional};
+use proc_macro2::{Span, TokenStream};
+use quote::quote;
+use syn::spanned::Spanned;
+use syn::{DataStruct, Fields};
+
+/// Generate a schema definition for a struct.
+pub fn get_struct_schema_def(
+    container_attrs: &NamedTypeOptions,
+    data_struct: DataStruct,
+    ident_span: Span,
+) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
+    let mut record_field_exprs = vec![];
+    match data_struct.fields {
+        Fields::Named(a) => {
+            for field in a.named {
+                let mut name = field
+                    .ident
+                    .as_ref()
+                    .expect("Field must have a name")
+                    .to_string();
+                if let Some(raw_name) = name.strip_prefix("r#") {
+                    name = raw_name.to_string();
+                }
+                let field_attrs = FieldOptions::new(&field.attrs, field.span())?;
+                let doc = preserve_optional(field_attrs.doc);
+                match (field_attrs.rename, container_attrs.rename_all) {
+                    (Some(rename), _) => {
+                        name = rename;
+                    }
+                    (None, rename_all) if rename_all != RenameRule::None => {
+                        name = rename_all.apply_to_field(&name);
+                    }
+                    _ => {}
+                }
+                if field_attrs.skip {
+                    continue;
+                } 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 get_record_fields =
+                        get_field_get_record_fields_expr(&field, field_attrs.with)?;
+                    record_field_exprs.push(quote! {
+                        if let Some(flattened_fields) = #get_record_fields {
+                            schema_fields.extend(flattened_fields);
+                        } else {
+                            panic!("{} does not have any fields to flatten to", stringify!(#field));
+                        }
+                    });
+
+                    // Don't add this field as it's been replaced by the child record fields
+                    continue;
+                }
+                let default_value = get_field_field_default_expr(&field, field_attrs.default)?;
+                let aliases = field_aliases(&field_attrs.alias);
+                let schema_expr = get_field_schema_expr(&field, field_attrs.with)?;
+                record_field_exprs.push(quote! {
+                    schema_fields.push(::apache_avro::schema::RecordField {
+                        name: #name.to_string(),
+                        doc: #doc,
+                        default: #default_value,
+                        aliases: #aliases,
+                        schema: #schema_expr,
+                        custom_attributes: ::std::collections::BTreeMap::new(),
+                    });
+                });
+            }
+        }
+        Fields::Unnamed(_) => {
+            return Err(vec![syn::Error::new(
+                ident_span,
+                "AvroSchema derive does not work for tuple structs",
+            )]);
+        }
+        Fields::Unit => {
+            return Err(vec![syn::Error::new(
+                ident_span,
+                "AvroSchema derive does not work for unit structs",
+            )]);
+        }
+    }
+
+    let record_doc = preserve_optional(container_attrs.doc.as_ref());
+    let record_aliases = aliases(&container_attrs.aliases);
+    let full_schema_name = &container_attrs.name;
+
+    // When flatten is involved, there will be more but we don't know how many. This optimises for
+    // the most common case where there is no flatten.
+    let minimum_fields = record_field_exprs.len();
+
+    let schema_def = quote! {
+        {
+            let mut schema_fields = ::std::vec::Vec::with_capacity(#minimum_fields);
+            #(#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:?}");
+            let name = ::apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to parse struct name for schema {}", #full_schema_name)[..]);
+            let lookup: ::std::collections::BTreeMap<String, usize> = schema_fields
+                .iter()
+                .enumerate()
+                .map(|(position, field)| (field.name.to_owned(), position))
+                .collect();
+            ::apache_avro::schema::Schema::Record(::apache_avro::schema::RecordSchema {
+                name,
+                aliases: #record_aliases,
+                doc: #record_doc,
+                fields: schema_fields,
+                lookup,
+                attributes: ::std::collections::BTreeMap::new(),
+            })
+        }
+    };
+    let record_fields = quote! {
+        let mut schema_fields = ::std::vec::Vec::with_capacity(#minimum_fields);
+        #(#record_field_exprs)*
+        ::std::option::Option::Some(schema_fields)
+    };
+
+    Ok((schema_def, record_fields))
+}
+
+/// Use the schema definition of the only field in the struct as the schema
+pub fn get_transparent_struct_schema_def(
+    fields: Fields,
+    input_span: Span,
+) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> {
+    match fields {
+        Fields::Named(fields_named) => {
+            let mut found = None;
+            for field in fields_named.named {
+                let attrs = FieldOptions::new(&field.attrs, field.span())?;
+                if attrs.skip {
+                    continue;
+                }
+                if found.replace((field, attrs)).is_some() {
+                    return Err(vec![syn::Error::new(
+                        input_span,
+                        "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field",
+                    )]);
+                }
+            }
+
+            if let Some((field, attrs)) = found {
+                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,
+                    "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field",
+                )])
+            }
+        }
+        Fields::Unnamed(_) => Err(vec![syn::Error::new(
+            input_span,
+            "AvroSchema: derive does not work for tuple structs",
+        )]),
+        Fields::Unit => Err(vec![syn::Error::new(
+            input_span,
+            "AvroSchema: derive does not work for unit structs",
+        )]),
+    }
+}
diff --git a/avro_derive/src/utils.rs b/avro_derive/src/utils.rs
new file mode 100644
index 0000000..fafc3b9
--- /dev/null
+++ b/avro_derive/src/utils.rs
@@ -0,0 +1,50 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+
+/// Convert a list of errors into actual compiler errors.
+///
+/// Copied from `serde`, originally written by David Tolnay.
+pub fn to_compile_errors(errors: impl AsRef<[syn::Error]>) -> TokenStream {
+    let compile_errors = errors.as_ref().iter().map(syn::Error::to_compile_error);
+    quote!(#(#compile_errors)*)
+}
+
+pub fn preserve_optional(op: Option<impl quote::ToTokens>) -> TokenStream {
+    if let Some(tt) = op {
+        quote! {::std::option::Option::Some(#tt.into())}
+    } else {
+        quote! {::std::option::Option::None}
+    }
+}
+
+/// Convert a list of strings to an expression that resolves to a `Option<Vec<Alias>>`.
+pub fn aliases(op: &[impl AsRef<str>]) -> TokenStream {
+    let items: Vec<TokenStream> = op
+        .iter()
+        .map(|alias| {
+            let alias = alias.as_ref();
+            quote! {
+                ::apache_avro::schema::Alias::new(#alias).expect("Alias is invalid")
+            }
+        })
+        .collect();
+    if items.is_empty() {
+        quote! {::std::option::Option::None}
+    } else {
+        quote! {::std::option::Option::Some(vec![#(#items),*])}
+    }
+}
+
+/// Convert a list of strings to an expression that resolves to a `Vec<String>`.
+pub fn field_aliases(op: &[impl AsRef<str>]) -> TokenStream {
+    let items: Vec<TokenStream> = op
+        .iter()
+        .map(|string| {
+            let string = string.as_ref();
+            quote! {
+                String::from(#string)
+            }
+        })
+        .collect();
+    quote! {vec![#(#items),*]}
+}
diff --git a/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs b/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs
index 0d35f4b..e73168a 100644
--- a/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs
+++ b/avro_derive/tests/expanded/avro_3709_record_field_attributes.expanded.rs
@@ -36,10 +36,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                         aliases: ::alloc::boxed::box_assume_init_into_vec_unsafe(
                             ::alloc::intrinsics::write_box_via_move(
                                 ::alloc::boxed::Box::new_uninit(),
-                                [
-                                    "a1".try_into().expect("Alias is invalid"),
-                                    "a2".try_into().expect("Alias is invalid"),
-                                ],
+                                [String::from("a1"), String::from("a2")],
                             ),
                         ),
                         schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
@@ -111,10 +108,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                 aliases: ::alloc::boxed::box_assume_init_into_vec_unsafe(
                     ::alloc::intrinsics::write_box_via_move(
                         ::alloc::boxed::Box::new_uninit(),
-                        [
-                            "a1".try_into().expect("Alias is invalid"),
-                            "a2".try_into().expect("Alias is invalid"),
-                        ],
+                        [String::from("a1"), String::from("a2")],
                     ),
                 ),
                 schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
diff --git a/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs b/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs
index d80a114..f13c46c 100644
--- a/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_207_rename_all_attribute.expanded.rs
@@ -29,7 +29,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                         name: "ITEM".to_string(),
                         doc: ::std::option::Option::None,
                         default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                             named_schemas,
                             enclosing_namespace,
@@ -41,7 +41,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                         name: "DOUBLE_ITEM".to_string(),
                         doc: ::std::option::Option::None,
                         default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                             named_schemas,
                             enclosing_namespace,
@@ -105,7 +105,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                 name: "ITEM".to_string(),
                 doc: ::std::option::Option::None,
                 default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                     named_schemas,
                     enclosing_namespace,
@@ -117,7 +117,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                 name: "DOUBLE_ITEM".to_string(),
                 doc: ::std::option::Option::None,
                 default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                     named_schemas,
                     enclosing_namespace,
diff --git a/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs b/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs
index 0689cb1..c26134c 100644
--- a/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_207_rename_attr_over_rename_all_attribute.expanded.rs
@@ -30,7 +30,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                         name: "ITEM".to_string(),
                         doc: ::std::option::Option::None,
                         default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                             named_schemas,
                             enclosing_namespace,
@@ -42,7 +42,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                         name: "DoubleItem".to_string(),
                         doc: ::std::option::Option::None,
                         default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                             named_schemas,
                             enclosing_namespace,
@@ -106,7 +106,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                 name: "ITEM".to_string(),
                 doc: ::std::option::Option::None,
                 default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                     named_schemas,
                     enclosing_namespace,
@@ -118,7 +118,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                 name: "DoubleItem".to_string(),
                 doc: ::std::option::Option::None,
                 default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                     named_schemas,
                     enclosing_namespace,
diff --git a/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs b/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs
index 34576a5..3562a9b 100644
--- a/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_501_basic.expanded.rs
@@ -28,7 +28,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                         name: "a".to_string(),
                         doc: ::std::option::Option::None,
                         default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                             named_schemas,
                             enclosing_namespace,
@@ -40,7 +40,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                         name: "b".to_string(),
                         doc: ::std::option::Option::None,
                         default: <String as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <String as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                             named_schemas,
                             enclosing_namespace,
@@ -104,7 +104,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                 name: "a".to_string(),
                 doc: ::std::option::Option::None,
                 default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                     named_schemas,
                     enclosing_namespace,
@@ -116,7 +116,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                 name: "b".to_string(),
                 doc: ::std::option::Option::None,
                 default: <String as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <String as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                     named_schemas,
                     enclosing_namespace,
diff --git a/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs b/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs
index ce84a5d..947173b 100644
--- a/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_501_namespace.expanded.rs
@@ -29,7 +29,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                         name: "a".to_string(),
                         doc: ::std::option::Option::None,
                         default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                             named_schemas,
                             enclosing_namespace,
@@ -41,7 +41,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                         name: "b".to_string(),
                         doc: ::std::option::Option::None,
                         default: <String as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <String as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                             named_schemas,
                             enclosing_namespace,
@@ -106,7 +106,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                 name: "a".to_string(),
                 doc: ::std::option::Option::None,
                 default: <i32 as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <i32 as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                     named_schemas,
                     enclosing_namespace,
@@ -118,7 +118,7 @@ impl ::apache_avro::AvroSchemaComponent for A {
                 name: "b".to_string(),
                 doc: ::std::option::Option::None,
                 default: <String as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <String as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                     named_schemas,
                     enclosing_namespace,
diff --git a/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs b/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs
index 9916d4f..a7e9293 100644
--- a/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_501_reference.expanded.rs
@@ -30,7 +30,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> {
                         default: <&'a Vec<
                             i32,
                         > as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <&'a Vec<
                             i32,
                         > as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
@@ -44,7 +44,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> {
                         name: "b".to_string(),
                         doc: ::std::option::Option::None,
                         default: <&'static str as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <&'static str as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                             named_schemas,
                             enclosing_namespace,
@@ -110,7 +110,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> {
                 default: <&'a Vec<
                     i32,
                 > as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <&'a Vec<
                     i32,
                 > as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
@@ -124,7 +124,7 @@ impl<'a> ::apache_avro::AvroSchemaComponent for A<'a> {
                 name: "b".to_string(),
                 doc: ::std::option::Option::None,
                 default: <&'static str as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <&'static str as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
                     named_schemas,
                     enclosing_namespace,
diff --git a/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs b/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs
index 076143e..e199f7c 100644
--- a/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs
+++ b/avro_derive/tests/expanded/avro_rs_501_struct_with_optional.expanded.rs
@@ -29,7 +29,7 @@ impl ::apache_avro::AvroSchemaComponent for TestOptional {
                         default: <Option<
                             i32,
                         > as ::apache_avro::AvroSchemaComponent>::field_default(),
-                        aliases: ::std::vec::Vec::new(),
+                        aliases: ::alloc::vec::Vec::new(),
                         schema: <Option<
                             i32,
                         > as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(
@@ -98,7 +98,7 @@ impl ::apache_avro::AvroSchemaComponent for TestOptional {
                 default: <Option<
                     i32,
                 > as ::apache_avro::AvroSchemaComponent>::field_default(),
-                aliases: ::std::vec::Vec::new(),
+                aliases: ::alloc::vec::Vec::new(),
                 schema: <Option<
                     i32,
                 > as ::apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(