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

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

commit 13b4e534b7153ed40dd960af53b5f70afceb6d82
Author: Kriskras99 <[email protected]>
AuthorDate: Wed Feb 11 15:18:55 2026 +0100

    feat: Allow types to provide default values
    
    `AvroSchemaComponent` is extended with a function `field_default`
    which will be called when deriving a record to set the default value
    for a field. The default implementation is to return `None`, which
    means no default.
    
    On the derive side, it is now possible to specify a default for a type
    using `#[avro(default = "..")]`. It is also possible to disable setting
    a default for a field with `#[avro(default = false)]`.
    
    This enables users to use `#[serde(skip_serializing{_if})]` on most fields
    without having to provide a default value.
---
 avro/src/serde/derive.rs                           | 103 ++++++++++-
 avro/src/types.rs                                  |   7 +
 avro_derive/src/attributes/avro.rs                 |   6 +-
 avro_derive/src/attributes/mod.rs                  |  54 +++++-
 avro_derive/src/lib.rs                             |  50 ++++-
 avro_derive/tests/derive.rs                        | 204 +++++++++++++++++++++
 .../tests/ui/avro_rs_226_skip_serializing.rs       |   1 +
 .../tests/ui/avro_rs_226_skip_serializing.stderr   |   5 +-
 .../tests/ui/avro_rs_226_skip_serializing_if.rs    |   1 +
 .../ui/avro_rs_226_skip_serializing_if.stderr      |   5 +-
 10 files changed, 412 insertions(+), 24 deletions(-)

diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs
index 066fe00..5d2fbca 100644
--- a/avro/src/serde/derive.rs
+++ b/avro/src/serde/derive.rs
@@ -22,6 +22,10 @@ use crate::schema::{
 use std::borrow::Cow;
 use std::collections::{HashMap, HashSet};
 
+const FIXED_8_DEFAULT: &str = "\0\0\0\0\0\0\0\0";
+const FIXED_12_DEFAULT: &str = "\0\0\0\0\0\0\0\0\0\0\0\0";
+const FIXED_16_DEFAULT: &str = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
+
 /// Trait for types that serve as an Avro data model.
 ///
 /// **Do not implement directly!** Either derive it or implement 
[`AvroSchemaComponent`] to get this trait
@@ -81,6 +85,10 @@ use std::collections::{HashMap, HashSet};
 ///
 ///    Set the `doc` attribute of the schema. Defaults to the documentation of 
the type.
 ///
+///  - `#[avro(default = r#"{"field": 42, "other": "Spam"}"#)]`
+///
+///    Provide the default value for this type when it is used in a field.
+///
 ///  - `#[avro(alias = "name")]`
 ///
 ///    Set the `alias` attribute of the schema. Can be specified multiple 
times.
@@ -113,11 +121,14 @@ use std::collections::{HashMap, HashSet};
 ///
 ///    Set the `doc` attribute of the field. Defaults to the documentation of 
the field.
 ///
-///  - `#[avro(default = "null")]`
+///  - `#[avro(default = "null")]` or `#[avro(default = false)]`
 ///
-///    Set the `default` attribute of the field.
+///    Control the `default` attribute of the field. When not used, it will 
use [`AvroSchemaComponent::field_default`]
+///    to get the default value for a type. This default value can be 
overriden by providing a JSON string.
+///    To remove the `default` attribute for a field, set `default` to `false`.
 ///
-///    _Note:_ This is a JSON value not a Rust value, as this is put in the 
schema itself.
+///    _Note:_ This is a JSON value not a Rust value, as this is put in the 
schema itself. To encode a JSON string
+///    you need to use double quotes: `#[avro(default = r#""Some string 
value""#)]`.
 ///
 ///  - `#[serde(alias = "name")]`
 ///
@@ -220,6 +231,11 @@ pub trait AvroSchema {
 ///     fn get_record_fields_in_ctxt(_: usize, _: &mut HashSet<Name>, _: 
&Namespace) -> Option<Vec<RecordField>> {
 ///         None // A Schema::Int is not a Schema::Record so there are no 
fields to return
 ///     }
+///
+///     fn field_default() -> Option<serde_json::Value> {
+///         // Zero as default value. Can also be None if you don't want to 
provide a default value
+///         Some(0u8.into())
+///     }
 ///}
 /// ```
 ///
@@ -242,6 +258,10 @@ pub trait AvroSchema {
 ///     fn get_record_fields_in_ctxt(first_field_position: usize, 
named_schemas: &mut HashSet<Name>, enclosing_namespace: &Namespace) -> 
Option<Vec<RecordField>> {
 ///         T::get_record_fields_in_ctxt(first_field_position, named_schemas, 
enclosing_namespace)
 ///     }
+///
+///     fn field_default() -> Option<serde_json::Value> {
+///         T::field_default()
+///     }
 ///}
 /// ```
 ///
@@ -256,6 +276,7 @@ pub trait AvroSchema {
 ///  - Implement `get_record_fields_in_ctxt` as the default implementation has 
to be implemented
 ///    with backtracking and a lot of cloning.
 ///      - Even if your schema is not a record, still implement the function 
and just return `None`
+///  - Implement `field_default()` if you want to use 
`#[serde(skip_serializing{,_if})]`.
 ///
 /// ```
 /// # use apache_avro::{Schema, serde::{AvroSchemaComponent}, schema::{Name, 
Namespace, RecordField, RecordSchema}};
@@ -305,6 +326,11 @@ pub trait AvroSchema {
 ///                 .build(),
 ///         ])
 ///     }
+///
+///     fn field_default() -> Option<serde_json::Value> {
+///         // This type does not provide a default value
+///         None
+///     }
 ///}
 /// ```
 pub trait AvroSchemaComponent {
@@ -332,6 +358,16 @@ pub trait AvroSchemaComponent {
             Self::get_schema_in_ctxt,
         )
     }
+
+    /// The default value of this type when used for a record field.
+    ///
+    /// `None` means no default value, which is also the default 
implementation.
+    ///
+    /// Implementations of this trait provided by this crate use the 
[`Default::default`] value of
+    /// the type.
+    fn field_default() -> Option<serde_json::Value> {
+        None
+    }
 }
 
 /// Get the record fields from `schema_fn` without polluting `named_schemas` 
or causing duplicate names
@@ -479,6 +515,10 @@ where
 
 macro_rules! impl_schema (
     ($type:ty, $variant_constructor:expr) => (
+        impl_schema!($type, $variant_constructor, <$type as 
Default>::default());
+    );
+
+    ($type:ty, $variant_constructor:expr, $default_constructor:expr) => (
         impl AvroSchemaComponent for $type {
             fn get_schema_in_ctxt(_: &mut HashSet<Name>, _: &Namespace) -> 
Schema {
                 $variant_constructor
@@ -487,6 +527,10 @@ macro_rules! impl_schema (
             fn get_record_fields_in_ctxt(_: usize, _: &mut HashSet<Name>, _: 
&Namespace) -> Option<Vec<RecordField>> {
                 None
             }
+
+            fn field_default() -> Option<serde_json::Value> {
+                Some(serde_json::Value::from($default_constructor))
+            }
         }
     );
 );
@@ -502,8 +546,8 @@ impl_schema!(u32, Schema::Long);
 impl_schema!(f32, Schema::Float);
 impl_schema!(f64, Schema::Double);
 impl_schema!(String, Schema::String);
-impl_schema!(str, Schema::String);
-impl_schema!(char, Schema::String);
+impl_schema!(str, Schema::String, String::default());
+impl_schema!(char, Schema::String, String::from(char::default()));
 
 macro_rules! impl_passthrough_schema (
     ($type:ty where T: AvroSchemaComponent + ?Sized $(+ $bound:tt)*) => (
@@ -515,6 +559,10 @@ macro_rules! impl_passthrough_schema (
             fn get_record_fields_in_ctxt(first_field_position: usize, 
named_schemas: &mut HashSet<Name>, enclosing_namespace: &Namespace) -> 
Option<Vec<RecordField>> {
                 T::get_record_fields_in_ctxt(first_field_position, 
named_schemas, enclosing_namespace)
             }
+
+            fn field_default() -> Option<serde_json::Value> {
+                T::field_default()
+            }
         }
     );
 );
@@ -535,6 +583,10 @@ macro_rules! impl_array_schema (
             fn get_record_fields_in_ctxt(_: usize, _: &mut HashSet<Name>, _: 
&Namespace) -> Option<Vec<RecordField>> {
                 None
             }
+
+            fn field_default() -> Option<serde_json::Value> {
+                Some(serde_json::Value::Array(Vec::new()))
+            }
         }
     );
 );
@@ -562,6 +614,13 @@ where
     ) -> Option<Vec<RecordField>> {
         None
     }
+
+    /// If `T` has a field default, this will return an array of with that 
default. Otherwise there is no default.
+    fn field_default() -> Option<serde_json::Value> {
+        T::field_default().map(|default| {
+            serde_json::Value::Array(std::array::from_fn::<_, N, _>(|_| 
default.clone()).to_vec())
+        })
+    }
 }
 
 impl<T> AvroSchemaComponent for HashMap<String, T>
@@ -582,6 +641,10 @@ where
     ) -> Option<Vec<RecordField>> {
         None
     }
+
+    fn field_default() -> Option<serde_json::Value> {
+        Some(serde_json::Value::Object(serde_json::Map::new()))
+    }
 }
 
 impl<T> AvroSchemaComponent for Option<T>
@@ -609,6 +672,10 @@ where
     ) -> Option<Vec<RecordField>> {
         None
     }
+
+    fn field_default() -> Option<serde_json::Value> {
+        Some(serde_json::Value::Null)
+    }
 }
 
 impl AvroSchemaComponent for core::time::Duration {
@@ -644,6 +711,10 @@ impl AvroSchemaComponent for core::time::Duration {
     ) -> Option<Vec<RecordField>> {
         None
     }
+
+    fn field_default() -> Option<serde_json::Value> {
+        Some(serde_json::Value::String(FIXED_12_DEFAULT.to_string()))
+    }
 }
 
 impl AvroSchemaComponent for uuid::Uuid {
@@ -679,6 +750,10 @@ impl AvroSchemaComponent for uuid::Uuid {
     ) -> Option<Vec<RecordField>> {
         None
     }
+
+    fn field_default() -> Option<serde_json::Value> {
+        Some(serde_json::Value::String(FIXED_16_DEFAULT.to_string()))
+    }
 }
 
 impl AvroSchemaComponent for u64 {
@@ -712,6 +787,10 @@ impl AvroSchemaComponent for u64 {
     ) -> Option<Vec<RecordField>> {
         None
     }
+
+    fn field_default() -> Option<serde_json::Value> {
+        Some(serde_json::Value::String(FIXED_8_DEFAULT.to_string()))
+    }
 }
 
 impl AvroSchemaComponent for u128 {
@@ -745,6 +824,10 @@ impl AvroSchemaComponent for u128 {
     ) -> Option<Vec<RecordField>> {
         None
     }
+
+    fn field_default() -> Option<serde_json::Value> {
+        Some(serde_json::Value::String(FIXED_16_DEFAULT.to_string()))
+    }
 }
 
 impl AvroSchemaComponent for i128 {
@@ -778,12 +861,18 @@ impl AvroSchemaComponent for i128 {
     ) -> Option<Vec<RecordField>> {
         None
     }
+
+    fn field_default() -> Option<serde_json::Value> {
+        Some(serde_json::Value::String(FIXED_16_DEFAULT.to_string()))
+    }
 }
 
 #[cfg(test)]
 mod tests {
-    use crate::schema::{FixedSchema, Name};
-    use crate::{AvroSchema, Schema};
+    use crate::{
+        AvroSchema, Schema,
+        schema::{FixedSchema, Name},
+    };
     use apache_avro_test_helper::TestResult;
 
     #[test]
diff --git a/avro/src/types.rs b/avro/src/types.rs
index 9c1f05f..f99dcb9 100644
--- a/avro/src/types.rs
+++ b/avro/src/types.rs
@@ -766,6 +766,13 @@ impl Value {
                 }
                 
Value::Uuid(Uuid::from_slice(bytes).map_err(Details::ConvertSliceToUuid)?)
             }
+            (Value::String(ref string), UuidSchema::Fixed(_)) => {
+                let bytes = string.as_bytes();
+                if bytes.len() != 16 {
+                    return 
Err(Details::ConvertFixedToUuid(bytes.len()).into());
+                }
+                
Value::Uuid(Uuid::from_slice(bytes).map_err(Details::ConvertSliceToUuid)?)
+            }
             (other, _) => return Err(Details::GetUuid(other).into()),
         };
         Ok(value)
diff --git a/avro_derive/src/attributes/avro.rs 
b/avro_derive/src/attributes/avro.rs
index ea171b5..fdf755e 100644
--- a/avro_derive/src/attributes/avro.rs
+++ b/avro_derive/src/attributes/avro.rs
@@ -21,6 +21,7 @@
 //! Although a user will mostly use the Serde attributes, there are some Avro 
specific attributes
 //! a user can use. These add extra metadata to the generated schema.
 
+use crate::attributes::FieldDefault;
 use crate::case::RenameRule;
 use darling::FromMeta;
 use proc_macro2::Span;
@@ -53,6 +54,9 @@ pub struct ContainerAttributes {
     /// [`serde::ContainerAttributes::rename_all`]: 
crate::attributes::serde::ContainerAttributes::rename_all
     #[darling(default)]
     pub rename_all: RenameRule,
+    /// Set the default value if this schema is used as a field
+    #[darling(default)]
+    pub default: Option<String>,
 }
 
 impl ContainerAttributes {
@@ -125,7 +129,7 @@ pub struct FieldAttributes {
     ///
     /// This is also used as the default when `skip_serializing{_if}` is used.
     #[darling(default)]
-    pub default: Option<String>,
+    pub default: FieldDefault,
     /// Deprecated. Use [`serde::FieldAttributes::alias`] instead.
     ///
     /// Adds the `aliases` field to the schema.
diff --git a/avro_derive/src/attributes/mod.rs 
b/avro_derive/src/attributes/mod.rs
index cc259f1..d05c8c3 100644
--- a/avro_derive/src/attributes/mod.rs
+++ b/avro_derive/src/attributes/mod.rs
@@ -17,7 +17,8 @@
 
 use crate::case::RenameRule;
 use darling::{FromAttributes, FromMeta};
-use proc_macro2::Span;
+use proc_macro2::{Span, TokenStream};
+use quote::quote;
 use syn::{AttrStyle, Attribute, Expr, Ident, Path, spanned::Spanned};
 
 mod avro;
@@ -30,6 +31,7 @@ pub struct NamedTypeOptions {
     pub aliases: Vec<String>,
     pub rename_all: RenameRule,
     pub transparent: bool,
+    pub default: TokenStream,
 }
 
 impl NamedTypeOptions {
@@ -116,12 +118,29 @@ impl NamedTypeOptions {
 
         let doc = avro.doc.or_else(|| extract_rustdoc(attributes));
 
+        let default = match avro.default {
+            None => quote! { None },
+            Some(default_value) => {
+                let _: serde_json::Value =
+                    serde_json::from_str(&default_value[..]).map_err(|e| {
+                        vec![syn::Error::new(
+                            ident.span(),
+                            format!("Invalid avro default json: \n{e}"),
+                        )]
+                    })?;
+                quote! {
+                    
Some(serde_json::from_str(#default_value).expect(format!("Invalid JSON: {:?}", 
#default_value).as_str()))
+                }
+            }
+        };
+
         Ok(Self {
             name: full_schema_name,
             doc,
             aliases: avro.alias,
             rename_all: serde.rename_all.serialize,
             transparent: serde.transparent,
+            default,
         })
     }
 }
@@ -210,11 +229,38 @@ impl With {
         }
     }
 }
+/// How to get the default value for a value.
+#[derive(Debug, PartialEq, Default)]
+pub enum FieldDefault {
+    /// Use `<T as AvroSchemaComponent>::field_default`.
+    #[default]
+    Trait,
+    /// Don't set a default.
+    Disabled,
+    /// Use this JSON value.
+    Value(String),
+}
+
+impl FromMeta for FieldDefault {
+    fn from_string(value: &str) -> darling::Result<Self> {
+        Ok(Self::Value(value.to_string()))
+    }
+
+    fn from_bool(value: bool) -> darling::Result<Self> {
+        if value {
+            Err(darling::Error::custom(
+                "Expected `false` or a JSON string, got `true`",
+            ))
+        } else {
+            Ok(Self::Disabled)
+        }
+    }
+}
 
 #[derive(Default)]
 pub struct FieldOptions {
     pub doc: Option<String>,
-    pub default: Option<String>,
+    pub default: FieldDefault,
     pub alias: Vec<String>,
     pub rename: Option<String>,
     pub skip: bool,
@@ -274,11 +320,11 @@ impl FieldOptions {
         }
         if ((serde.skip_serializing && !serde.skip_deserializing)
             || serde.skip_serializing_if.is_some())
-            && avro.default.is_none()
+            && avro.default == FieldDefault::Disabled
         {
             errors.push(syn::Error::new(
                 span,
-                "`#[serde(skip_serializing)]` and 
`#[serde(skip_serializing_if)]` require `#[avro(default = \"..\")]`"
+                "`#[serde(skip_serializing)]` and 
`#[serde(skip_serializing_if)]` are incompatible with `#[avro(default = 
false)]`"
             ));
         }
         let with = match With::from_avro_and_serde(&avro.with, &serde.with, 
span) {
diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs
index df14b97..f0bcbc2 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -40,7 +40,7 @@ use syn::{
 };
 
 use crate::{
-    attributes::{FieldOptions, NamedTypeOptions, VariantOptions, With},
+    attributes::{FieldDefault, FieldOptions, NamedTypeOptions, VariantOptions, 
With},
     case::RenameRule,
 };
 
@@ -75,6 +75,7 @@ fn derive_avro_schema(input: DeriveInput) -> 
Result<TokenStream, Vec<syn::Error>
                 &input.generics,
                 get_schema_impl,
                 get_record_fields_impl,
+                named_type_options.default,
             ))
         }
         syn::Data::Enum(data_enum) => {
@@ -93,6 +94,7 @@ fn derive_avro_schema(input: DeriveInput) -> 
Result<TokenStream, Vec<syn::Error>
                 &input.generics,
                 inner,
                 quote! { None },
+                named_type_options.default,
             ))
         }
         syn::Data::Union(_) => Err(vec![syn::Error::new(
@@ -108,6 +110,7 @@ fn create_trait_definition(
     generics: &Generics,
     get_schema_impl: TokenStream,
     get_record_fields_impl: TokenStream,
+    field_default_impl: TokenStream,
 ) -> TokenStream {
     let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
     quote! {
@@ -120,6 +123,10 @@ fn create_trait_definition(
             fn get_record_fields_in_ctxt(mut field_position: usize, 
named_schemas: &mut ::std::collections::HashSet<::apache_avro::schema::Name>, 
enclosing_namespace: &::std::option::Option<::std::string::String>) -> 
::std::option::Option<::std::vec::Vec<::apache_avro::schema::RecordField>> {
                 #get_record_fields_impl
             }
+
+            fn field_default() -> ::std::option::Option<::serde_json::Value> {
+                ::std::option::Option::#field_default_impl
+            }
         }
     }
 }
@@ -187,7 +194,9 @@ fn get_struct_schema_def(
                     continue;
                 }
                 let default_value = match field_attrs.default {
-                    Some(default_value) => {
+                    FieldDefault::Disabled => quote! { 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(
@@ -199,7 +208,6 @@ fn get_struct_schema_def(
                             
Some(serde_json::from_str(#default_value).expect(format!("Invalid JSON: {:?}", 
#default_value).as_str()))
                         }
                     }
-                    None => quote! { None },
                 };
                 let aliases = aliases(&field_attrs.alias);
                 let schema_expr = get_field_schema_expr(&field, 
field_attrs.with)?;
@@ -462,6 +470,28 @@ fn type_to_get_record_fields_expr(ty: &Type) -> 
Result<TokenStream, Vec<syn::Err
     }
 }
 
+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:?}"
+            ),
+        )]),
+    }
+}
+
 fn default_enum_variant(
     data_enum: &syn::DataEnum,
     error_span: Span,
@@ -664,6 +694,10 @@ mod tests {
                         ) -> ::std::option::Option 
<::std::vec::Vec<::apache_avro::schema::RecordField>> {
                             None
                         }
+
+                        fn field_default () -> 
::std::option::Option<::serde_json::Value> {
+                            ::std::option::Option::None
+                        }
                     }
                 }.to_string());
             }
@@ -790,7 +824,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 :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
:: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt 
(named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -809,7 +843,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 :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
:: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt 
(named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -832,7 +866,7 @@ mod tests {
         match syn::parse2::<DeriveInput>(test_struct) {
             Ok(input) => {
                 let schema_res = derive_avro_schema(input);
-                let expected_token_stream = r#"# [automatically_derived] impl 
:: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt 
(named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
:: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt 
(named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -852,7 +886,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 :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
:: apache_avro :: AvroSchemaComponent for B { fn get_schema_in_ctxt 
(named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
                 let schema_token_stream = schema_res.unwrap().to_string();
                 assert_eq!(schema_token_stream, expected_token_stream);
             }
@@ -876,7 +910,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 :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
+                let expected_token_stream = r#"# [automatically_derived] impl 
:: apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt 
(named_schemas : & mut :: std :: collections :: HashSet < :: apache_avro :: 
schema :: Name > , 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_qualifi [...]
                 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 631e825..7f536b6 100644
--- a/avro_derive/tests/derive.rs
+++ b/avro_derive/tests/derive.rs
@@ -25,7 +25,9 @@ use std::{
     borrow::Cow,
     collections::{HashMap, HashSet},
     sync::Mutex,
+    time::Duration,
 };
+use uuid::Uuid;
 
 use pretty_assertions::assert_eq;
 
@@ -1369,6 +1371,7 @@ fn test_basic_struct_with_defaults() {
         #[avro(default = "true")]
         condition: bool,
         // no default value for 'c'
+        #[avro(default = false)]
         c: f64,
         #[avro(default = r#"{"a": 1, "b": 2}"#)]
         map: HashMap<String, i32>,
@@ -1942,6 +1945,7 @@ fn avro_rs_397_uuid() {
                     "type":"fixed",
                     "logicalType":"uuid",
                     "name":"uuid",
+                    
"default":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
                     "size":16
                 }
             }
@@ -2374,3 +2378,203 @@ fn avro_rs_448_flatten_field_positions() {
         .collect::<Vec<_>>();
     assert_eq!(positions.as_slice(), &[0, 1, 2, 3][..]);
 }
+
+#[test]
+fn avro_rs_476_field_default() {
+    #[derive(AvroSchema)]
+    struct Bar {
+        _field: Box<Bar>,
+    }
+
+    #[derive(AvroSchema)]
+    #[avro(default = r#"{"_field": true}"#)]
+    struct Spam {
+        _field: bool,
+    }
+
+    #[derive(AvroSchema)]
+    struct Foo {
+        _a: bool,
+        _b: i8,
+        _c: i16,
+        _d: i32,
+        _e: i64,
+        _f: u8,
+        _g: u16,
+        _h: u32,
+        _i: f32,
+        _j: f64,
+        _k: String,
+        _l: Box<str>,
+        _m: char,
+        _n: Box<Spam>,
+        _o: Vec<bool>,
+        _p: [u8; 5],
+        _p_alt: [Bar; 5],
+        _q: HashMap<String, String>,
+        _r: Option<f64>,
+        _s: Duration,
+        _t: Uuid,
+        _u: u64,
+        _v: u128,
+        _w: i128,
+        _x: Bar,
+    }
+
+    let schema = Foo::get_schema();
+    assert_eq!(
+        serde_json::to_string(&schema).unwrap(),
+        
r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean","default":false},{"name":"_b","type":"int","default":0},{"name":"_c","type":"int","default":0},{"name":"_d","type":"int","default":0},{"name":"_e","type":"long","default":0},{"name":"_f","type":"int","default":0},{"name":"_g","type":"int","default":0},{"name":"_h","type":"long","default":0},{"name":"_i","type":"float","default":0.0},{"name":"_j","type":"double","default":0.0},{"name":"_k","type":"string","de
 [...]
+    );
+}
+
+#[test]
+fn avro_rs_476_field_default_false() {
+    #[derive(AvroSchema)]
+    struct Bar {
+        _field: Box<Bar>,
+    }
+
+    #[derive(AvroSchema)]
+    #[avro(default = r#"{"_field": true}"#)]
+    struct Spam {
+        _field: bool,
+    }
+
+    #[derive(AvroSchema)]
+    struct Foo {
+        #[avro(default = false)]
+        _a: bool,
+        #[avro(default = false)]
+        _b: i8,
+        #[avro(default = false)]
+        _c: i16,
+        #[avro(default = false)]
+        _d: i32,
+        #[avro(default = false)]
+        _e: i64,
+        #[avro(default = false)]
+        _f: u8,
+        #[avro(default = false)]
+        _g: u16,
+        #[avro(default = false)]
+        _h: u32,
+        #[avro(default = false)]
+        _i: f32,
+        #[avro(default = false)]
+        _j: f64,
+        #[avro(default = false)]
+        _k: String,
+        #[avro(default = false)]
+        _l: Box<str>,
+        #[avro(default = false)]
+        _m: char,
+        #[avro(default = false)]
+        _n: Box<Spam>,
+        #[avro(default = false)]
+        _o: Vec<bool>,
+        #[avro(default = false)]
+        _p: [u8; 5],
+        #[avro(default = false)]
+        _p_alt: [Bar; 5],
+        #[avro(default = false)]
+        _q: HashMap<String, String>,
+        #[avro(default = false)]
+        _r: Option<f64>,
+        #[avro(default = false)]
+        _s: Duration,
+        #[avro(default = false)]
+        _t: Uuid,
+        #[avro(default = false)]
+        _u: u64,
+        #[avro(default = false)]
+        _v: u128,
+        #[avro(default = false)]
+        _w: i128,
+        #[avro(default = false)]
+        _x: Bar,
+    }
+
+    let schema = Foo::get_schema();
+    assert_eq!(
+        serde_json::to_string(&schema).unwrap(),
+        
r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean"},{"name":"_b","type":"int"},{"name":"_c","type":"int"},{"name":"_d","type":"int"},{"name":"_e","type":"long"},{"name":"_f","type":"int"},{"name":"_g","type":"int"},{"name":"_h","type":"long"},{"name":"_i","type":"float"},{"name":"_j","type":"double"},{"name":"_k","type":"string"},{"name":"_l","type":"string"},{"name":"_m","type":"string"},{"name":"_n","type":{"type":"record","name":"Spam","fields":[{"name":"
 [...]
+    );
+}
+
+#[test]
+fn avro_rs_476_field_default_provided() {
+    #[derive(AvroSchema)]
+    #[avro(default = r#"{"_field": true}"#)]
+    struct Spam {
+        _field: bool,
+    }
+
+    #[derive(AvroSchema)]
+    struct Foo {
+        #[avro(default = "true")]
+        _a: bool,
+        #[avro(default = "42")]
+        _b: i8,
+        #[avro(default = "42")]
+        _c: i16,
+        #[avro(default = "42")]
+        _d: i32,
+        #[avro(default = "42")]
+        _e: i64,
+        #[avro(default = "42")]
+        _f: u8,
+        #[avro(default = "42")]
+        _g: u16,
+        #[avro(default = "42")]
+        _h: u32,
+        #[avro(default = "42.0")]
+        _i: f32,
+        #[avro(default = "42.0")]
+        _j: f64,
+        #[avro(default = r#""String""#)]
+        _k: String,
+        #[avro(default = r#""str""#)]
+        _l: Box<str>,
+        #[avro(default = r#""Z""#)]
+        _m: char,
+        #[avro(default = r#"{"_field": false}"#)]
+        _n: Box<Spam>,
+        #[avro(default = "[true, false, true]")]
+        _o: Vec<bool>,
+        #[avro(default = "[1,2,3,4,5]")]
+        _p: [u8; 5],
+        #[avro(
+            default = r#"[{"_field": true},{"_field": false},{"_field": 
true},{"_field": false},{"_field": true}]"#
+        )]
+        _p_alt: [Spam; 5],
+        #[avro(default = r#"{"A": "B"}"#)]
+        _q: HashMap<String, String>,
+        #[avro(default = "42.0")]
+        _r: Option<f64>,
+        #[avro(
+            default = 
r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""#
+        )]
+        _s: Duration,
+        #[avro(
+            default = 
r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""#
+        )]
+        _t: Uuid,
+        #[avro(default = 
r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""#)]
+        _u: u64,
+        #[avro(
+            default = 
r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""#
+        )]
+        _v: u128,
+        #[avro(
+            default = 
r#""\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0001""#
+        )]
+        _w: i128,
+    }
+
+    let schema = Foo::get_schema();
+    assert_eq!(
+        serde_json::to_string(&schema).unwrap(),
+        
r#"{"type":"record","name":"Foo","fields":[{"name":"_a","type":"boolean","default":true},{"name":"_b","type":"int","default":42},{"name":"_c","type":"int","default":42},{"name":"_d","type":"int","default":42},{"name":"_e","type":"long","default":42},{"name":"_f","type":"int","default":42},{"name":"_g","type":"int","default":42},{"name":"_h","type":"long","default":42},{"name":"_i","type":"float","default":42.0},{"name":"_j","type":"double","default":42.0},{"name":"_k","type":"str
 [...]
+    );
+}
diff --git a/avro_derive/tests/ui/avro_rs_226_skip_serializing.rs 
b/avro_derive/tests/ui/avro_rs_226_skip_serializing.rs
index 40f4756..28a7395 100644
--- a/avro_derive/tests/ui/avro_rs_226_skip_serializing.rs
+++ b/avro_derive/tests/ui/avro_rs_226_skip_serializing.rs
@@ -22,6 +22,7 @@ struct T {
     x: Option<i8>,
     y: Option<String>,
     #[serde(skip_serializing)]
+    #[avro(default = false)]
     z: Option<i8>,
 }
 
diff --git a/avro_derive/tests/ui/avro_rs_226_skip_serializing.stderr 
b/avro_derive/tests/ui/avro_rs_226_skip_serializing.stderr
index e974f85..d5316b5 100644
--- a/avro_derive/tests/ui/avro_rs_226_skip_serializing.stderr
+++ b/avro_derive/tests/ui/avro_rs_226_skip_serializing.stderr
@@ -1,6 +1,7 @@
-error: `#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` 
require `#[avro(default = "..")]`
+error: `#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` are 
incompatible with `#[avro(default = false)]`
   --> tests/ui/avro_rs_226_skip_serializing.rs:24:5
    |
 24 | /     #[serde(skip_serializing)]
-25 | |     z: Option<i8>,
+25 | |     #[avro(default = false)]
+26 | |     z: Option<i8>,
    | |_________________^
diff --git a/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.rs 
b/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.rs
index 2e87a9c..e532066 100644
--- a/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.rs
+++ b/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.rs
@@ -21,6 +21,7 @@ use apache_avro::AvroSchema;
 struct T {
     x: Option<i8>,
     #[serde(skip_serializing_if = "Option::is_none")]
+    #[avro(default = false)]
     y: Option<String>,
     z: Option<i8>,
 }
diff --git a/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.stderr 
b/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.stderr
index 9794bfb..f3094d0 100644
--- a/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.stderr
+++ b/avro_derive/tests/ui/avro_rs_226_skip_serializing_if.stderr
@@ -1,6 +1,7 @@
-error: `#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` 
require `#[avro(default = "..")]`
+error: `#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` are 
incompatible with `#[avro(default = false)]`
   --> tests/ui/avro_rs_226_skip_serializing_if.rs:23:5
    |
 23 | /     #[serde(skip_serializing_if = "Option::is_none")]
-24 | |     y: Option<String>,
+24 | |     #[avro(default = false)]
+25 | |     y: Option<String>,
    | |_____________________^


Reply via email to