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

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

commit 49353fac7bbd262b46c041359bdbb64666f9026b
Author: default <[email protected]>
AuthorDate: Fri Jan 9 13:31:30 2026 +0000

    feat(derive): Allow overriding the schema used for a field
---
 avro_derive/src/attributes/avro.rs                 | 27 +++++++++++
 avro_derive/src/attributes/mod.rs                  | 55 ++++++++++++++++++++--
 avro_derive/src/attributes/serde.rs                |  3 +-
 avro_derive/src/lib.rs                             | 10 +++-
 avro_derive/tests/derive.rs                        | 54 ++++++++++++++++++++-
 .../tests/ui/avro_rs_396_with_expr_string.rs       | 27 +++++++++++
 .../tests/ui/avro_rs_396_with_expr_string.stderr   |  8 ++++
 avro_derive/tests/ui/avro_rs_396_with_expr_type.rs | 27 +++++++++++
 .../tests/ui/avro_rs_396_with_expr_type.stderr     |  8 ++++
 .../ui/avro_rs_396_with_word_without_serde.rs      | 27 +++++++++++
 .../ui/avro_rs_396_with_word_without_serde.stderr  |  6 +++
 11 files changed, 243 insertions(+), 9 deletions(-)

diff --git a/avro_derive/src/attributes/avro.rs 
b/avro_derive/src/attributes/avro.rs
index ceeafe5..b4b880c 100644
--- a/avro_derive/src/attributes/avro.rs
+++ b/avro_derive/src/attributes/avro.rs
@@ -22,7 +22,9 @@
 //! a user can use. These add extra metadata to the generated schema.
 
 use crate::case::RenameRule;
+use darling::FromMeta;
 use proc_macro2::Span;
+use syn::Expr;
 
 /// All the Avro attributes a container can have.
 #[derive(darling::FromAttributes)]
@@ -97,6 +99,21 @@ impl VariantAttributes {
     }
 }
 
+/// How to get the schema for this field.
+#[derive(Debug, FromMeta, PartialEq, Default)]
+#[darling(from_expr = |expr| Ok(With::Expr(expr.clone())))]
+pub enum With {
+    /// Use `<T as AvroSchemaComponent>::get_schema_with_ctxt`.
+    #[default]
+    #[darling(skip)]
+    Trait,
+    /// Use `module::get_schema_with_ctxt` where the module is defined by 
Serde's `with` attribute.
+    #[darling(word, skip)]
+    Serde,
+    /// Call the function in this expression.
+    Expr(Expr),
+}
+
 /// All the Avro attributes a field can have.
 #[derive(darling::FromAttributes)]
 #[darling(attributes(avro))]
@@ -137,6 +154,16 @@ pub struct FieldAttributes {
     /// [`serde::FieldAttributes::flatten`]: 
crate::attributes::serde::FieldAttributes::flatten
     #[darling(default)]
     pub flatten: bool,
+    /// How to get the schema for this field.
+    ///
+    /// By default uses `<T as AvroSchemaComponent>::get_schema_in_ctxt`.
+    ///
+    /// When it's provided without an argument (`#[avro(with)]`), it will use 
the function `get_schema_in_ctxt` defined
+    /// in the same module as the `#[serde(with = "..")]` attribute.
+    ///
+    /// When it's provided with an argument (`#[avro(with = ..)]`), it will 
use that function.
+    #[darling(default)]
+    pub with: With,
 }
 
 impl FieldAttributes {
diff --git a/avro_derive/src/attributes/mod.rs 
b/avro_derive/src/attributes/mod.rs
index 6f26a62..e66fc0e 100644
--- a/avro_derive/src/attributes/mod.rs
+++ b/avro_derive/src/attributes/mod.rs
@@ -16,12 +16,12 @@
 // under the License.
 
 use crate::case::RenameRule;
-use darling::FromAttributes;
+use darling::{FromAttributes, FromMeta};
 use proc_macro2::Span;
-use syn::{Attribute, spanned::Spanned};
+use syn::{Attribute, Expr, Path, spanned::Spanned};
 
-pub mod avro;
-pub mod serde;
+mod avro;
+mod serde;
 
 #[derive(Default)]
 pub struct NamedTypeOptions {
@@ -128,6 +128,7 @@ impl VariantOptions {
             ));
         }
 
+        // Check for conflicts between Serde and Avro
         if avro.rename.is_some() && serde.rename != avro.rename {
             errors.push(syn::Error::new(
                 span,
@@ -145,6 +146,42 @@ impl VariantOptions {
     }
 }
 
+/// How to get the schema for this field or variant.
+#[derive(Debug, PartialEq, Default)]
+pub enum With {
+    /// Use `<T as AvroSchemaComponent>::get_schema_with_ctxt`.
+    #[default]
+    Trait,
+    /// Use `module::get_schema_with_ctxt` where the module is defined by 
Serde's `with` attribute.
+    Serde(Path),
+    /// Call the function in this expression.
+    Expr(Expr),
+}
+
+impl With {
+    fn from_avro_and_serde(
+        avro: &avro::With,
+        serde: &Option<String>,
+        span: Span,
+    ) -> Result<Self, syn::Error> {
+        match &avro {
+            avro::With::Trait => Ok(Self::Trait),
+            avro::With::Serde => {
+                if let Some(serde) = serde {
+                    let path = Path::from_string(serde).unwrap();
+                    Ok(Self::Serde(path))
+                } else {
+                    Err(syn::Error::new(
+                        span,
+                        "`#[avro(with)]` requires `#[serde(with = \"..\")]` or 
provide a function to call `#[avro(width = ..)]`",
+                    ))
+                }
+            }
+            avro::With::Expr(expr) => Ok(Self::Expr(expr.clone())),
+        }
+    }
+}
+
 pub struct FieldOptions {
     pub doc: Option<String>,
     pub default: Option<String>,
@@ -152,6 +189,7 @@ pub struct FieldOptions {
     pub rename: Option<String>,
     pub skip: bool,
     pub flatten: bool,
+    pub with: With,
 }
 
 impl FieldOptions {
@@ -213,6 +251,14 @@ impl FieldOptions {
                 "`#[serde(skip_serializing)]` and 
`#[serde(skip_serializing_if)]` require `#[avro(default = \"..\")]`"
             ));
         }
+        let with = match With::from_avro_and_serde(&avro.with, &serde.with, 
span) {
+            Ok(with) => with,
+            Err(error) => {
+                errors.push(error);
+                // This won't actually be used, but it does simplify the code
+                With::Trait
+            }
+        };
 
         if !errors.is_empty() {
             return Err(errors);
@@ -225,6 +271,7 @@ impl FieldOptions {
             rename: serde.rename,
             skip: serde.skip || (serde.skip_serializing && 
serde.skip_deserializing),
             flatten: serde.flatten,
+            with,
         })
     }
 }
diff --git a/avro_derive/src/attributes/serde.rs 
b/avro_derive/src/attributes/serde.rs
index 9f23f83..b1ca6fa 100644
--- a/avro_derive/src/attributes/serde.rs
+++ b/avro_derive/src/attributes/serde.rs
@@ -238,8 +238,7 @@ pub struct FieldAttributes {
     #[darling(rename = "deserialize_with")]
     pub _deserialize_with: Option<String>,
     /// Use this module for (de)serializing.
-    #[darling(rename = "with")]
-    pub _with: Option<String>,
+    pub with: Option<String>,
     /// Put bounds on the lifetimes.
     #[darling(rename = "borrow")]
     pub _borrow: Option<SerdeBorrow>,
diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs
index 8723593..b4e500b 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -28,7 +28,7 @@ use syn::{
 };
 
 use crate::{
-    attributes::{FieldOptions, NamedTypeOptions, VariantOptions},
+    attributes::{FieldOptions, NamedTypeOptions, VariantOptions, With},
     case::RenameRule,
 };
 
@@ -167,7 +167,13 @@ fn get_data_struct_schema_def(
                     None => quote! { None },
                 };
                 let aliases = preserve_vec(field_attrs.alias);
-                let schema_expr = type_to_schema_expr(&field.ty)?;
+                let schema_expr = match field_attrs.with {
+                    With::Trait => type_to_schema_expr(&field.ty)?,
+                    With::Serde(path) => {
+                        quote! {#path::get_schema_in_ctxt(named_schemas, 
enclosing_namespace)}
+                    }
+                    With::Expr(expr) => quote! {#expr(named_schemas, 
enclosing_namespace)},
+                };
                 record_field_exprs.push(quote! {
                     schema_fields.push(::apache_avro::schema::RecordField {
                         name: #name.to_string(),
diff --git a/avro_derive/tests/derive.rs b/avro_derive/tests/derive.rs
index 72c6db0..e6978e5 100644
--- a/avro_derive/tests/derive.rs
+++ b/avro_derive/tests/derive.rs
@@ -17,7 +17,7 @@
 
 use apache_avro::{
     Reader, Schema, Writer, from_value,
-    schema::{AvroSchema, derive::AvroSchemaComponent},
+    schema::{AvroSchema, Name, Namespace, derive::AvroSchemaComponent},
 };
 use apache_avro_derive::*;
 use proptest::prelude::*;
@@ -1827,3 +1827,55 @@ fn avro_rs_247_serde_flatten_support_with_skip() {
         b: 321,
     });
 }
+
+#[test]
+fn avro_rs_396_with() {
+    let schema = Schema::parse_str(
+        r#"
+    {
+        "type":"record",
+        "name":"Foo",
+        "fields": [
+            {
+                "name":"a",
+                "type":"bytes"
+            },
+            {
+                "name":"b",
+                "type":"long"
+            }
+        ]
+    }
+    "#,
+    )
+    .unwrap();
+
+    fn long_schema(
+        _named_schemas: &mut HashMap<Name, Schema>,
+        _enclosing_namespace: &Namespace,
+    ) -> Schema {
+        Schema::Long
+    }
+
+    mod module {
+        use super::*;
+        pub fn get_schema_in_ctxt(
+            _named_schemas: &mut HashMap<Name, Schema>,
+            _enclosing_namespace: &Namespace,
+        ) -> Schema {
+            Schema::Bytes
+        }
+    }
+
+    #[allow(dead_code)]
+    #[derive(AvroSchema)]
+    struct Foo {
+        #[avro(with)]
+        #[serde(with = "module")]
+        a: String,
+        #[avro(with = long_schema)]
+        b: i32,
+    }
+
+    assert_eq!(schema, Foo::get_schema());
+}
diff --git a/avro_derive/tests/ui/avro_rs_396_with_expr_string.rs 
b/avro_derive/tests/ui/avro_rs_396_with_expr_string.rs
new file mode 100644
index 0000000..dd24c69
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_396_with_expr_string.rs
@@ -0,0 +1,27 @@
+// 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::AvroSchema;
+
+#[derive(AvroSchema)]
+struct Foo {
+    #[avro(with = "Schema::Bytes")]
+    a: String,
+    b: i32,
+}
+
+pub fn main() {}
diff --git a/avro_derive/tests/ui/avro_rs_396_with_expr_string.stderr 
b/avro_derive/tests/ui/avro_rs_396_with_expr_string.stderr
new file mode 100644
index 0000000..68a0aac
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_396_with_expr_string.stderr
@@ -0,0 +1,8 @@
+error[E0618]: expected function, found `&'static str`
+  --> tests/ui/avro_rs_396_with_expr_string.rs:22:19
+   |
+20 | #[derive(AvroSchema)]
+   |          ---------- call expression requires function
+21 | struct Foo {
+22 |     #[avro(with = "Schema::Bytes")]
+   |                   ^^^^^^^^^^^^^^^
diff --git a/avro_derive/tests/ui/avro_rs_396_with_expr_type.rs 
b/avro_derive/tests/ui/avro_rs_396_with_expr_type.rs
new file mode 100644
index 0000000..93829a4
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_396_with_expr_type.rs
@@ -0,0 +1,27 @@
+// 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::{AvroSchema, Schema};
+
+#[derive(AvroSchema)]
+struct Foo {
+    #[avro(with = Schema::Bytes)]
+    a: String,
+    b: i32,
+}
+
+pub fn main() {}
diff --git a/avro_derive/tests/ui/avro_rs_396_with_expr_type.stderr 
b/avro_derive/tests/ui/avro_rs_396_with_expr_type.stderr
new file mode 100644
index 0000000..02b8bfd
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_396_with_expr_type.stderr
@@ -0,0 +1,8 @@
+error[E0618]: expected function, found `Schema`
+  --> tests/ui/avro_rs_396_with_expr_type.rs:22:19
+   |
+20 | #[derive(AvroSchema)]
+   |          ---------- call expression requires function
+21 | struct Foo {
+22 |     #[avro(with = Schema::Bytes)]
+   |                   ^^^^^^^^^^^^^
diff --git a/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.rs 
b/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.rs
new file mode 100644
index 0000000..eb44682
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.rs
@@ -0,0 +1,27 @@
+// 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::AvroSchema;
+
+#[derive(AvroSchema)]
+struct Foo {
+    #[avro(with)]
+    a: String,
+    b: i32,
+}
+
+pub fn main() {}
diff --git a/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.stderr 
b/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.stderr
new file mode 100644
index 0000000..0527568
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_396_with_word_without_serde.stderr
@@ -0,0 +1,6 @@
+error: `#[avro(with)]` requires `#[serde(with = "..")]` or provide a function 
to call `#[avro(width = ..)]`
+  --> tests/ui/avro_rs_396_with_word_without_serde.rs:22:5
+   |
+22 | /     #[avro(with)]
+23 | |     a: String,
+   | |_____________^

Reply via email to