This is an automated email from the ASF dual-hosted git repository.
mgrigorov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 283c4b1 feat(derive): allow overriding the schema for a field and
change the schema for `uuid::Uuid` (#397)
283c4b1 is described below
commit 283c4b168fa6f4fd83adbc7a7257bf353b9ed20a
Author: Kriskras99 <[email protected]>
AuthorDate: Fri Jan 16 16:37:33 2026 +0100
feat(derive): allow overriding the schema for a field and change the schema
for `uuid::Uuid` (#397)
---
Cargo.lock | 1 +
Cargo.toml | 3 +-
avro/Cargo.toml | 2 +-
avro/src/bytes.rs | 123 ++++++++++++--
avro/src/schema.rs | 65 ++++++--
avro_derive/Cargo.toml | 1 +
avro_derive/src/attributes/avro.rs | 27 ++++
avro_derive/src/attributes/mod.rs | 60 ++++++-
avro_derive/src/attributes/serde.rs | 3 +-
avro_derive/src/lib.rs | 116 +++++++++-----
avro_derive/tests/derive.rs | 178 ++++++++++++++++++++-
avro_derive/tests/serde.rs | 70 ++++++++
.../ui/avro_rs_397_with_closure_parameters.rs | 27 ++++
.../ui/avro_rs_397_with_closure_parameters.stderr | 14 ++
.../tests/ui/avro_rs_397_with_expr_string.rs | 27 ++++
.../tests/ui/avro_rs_397_with_expr_string.stderr | 6 +
avro_derive/tests/ui/avro_rs_397_with_expr_type.rs | 27 ++++
.../tests/ui/avro_rs_397_with_expr_type.stderr | 8 +
.../ui/avro_rs_397_with_word_without_serde.rs | 27 ++++
.../ui/avro_rs_397_with_word_without_serde.stderr | 6 +
20 files changed, 709 insertions(+), 82 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index b219c04..80daff1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -105,6 +105,7 @@ dependencies = [
"serde_json",
"syn",
"trybuild",
+ "uuid",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 0d8acc6..f5a1231 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,10 +41,11 @@ documentation = "https://docs.rs/apache-avro"
# dependencies used by more than one members
[workspace.dependencies]
log = { default-features = false, version = "0.4.29" }
+pretty_assertions = { default-features = false, version = "1.4.1", features =
["std"] }
serde = { default-features = false, version = "1.0.228", features = ["std",
"derive"] }
serde_bytes = { default-features = false, version = "0.11.19", features =
["std"] }
serde_json = { default-features = false, version = "1.0.149", features =
["std"] }
-pretty_assertions = { default-features = false, version = "1.4.1", features =
["std"] }
+uuid = { default-features = false, version = "1.19.0", features = ["serde",
"std"] }
[profile.release.package.hello-wasm]
# Tell `rustc` to optimize for small code size.
diff --git a/avro/Cargo.toml b/avro/Cargo.toml
index 7133ef7..8c50fd3 100644
--- a/avro/Cargo.toml
+++ b/avro/Cargo.toml
@@ -70,7 +70,7 @@ snap = { default-features = false, version = "1.1.0",
optional = true }
strum = { default-features = false, version = "0.27.2" }
strum_macros = { default-features = false, version = "0.27.2" }
thiserror = { default-features = false, version = "2.0.17" }
-uuid = { default-features = false, version = "1.19.0", features = ["serde",
"std"] }
+uuid = { workspace = true }
liblzma = { default-features = false, version = "0.4.5", optional = true }
zstd = { default-features = false, version = "0.13.3", optional = true }
diff --git a/avro/src/bytes.rs b/avro/src/bytes.rs
index 8ac98b5..bac4e3f 100644
--- a/avro/src/bytes.rs
+++ b/avro/src/bytes.rs
@@ -44,14 +44,16 @@ pub(crate) enum BytesType {
///
/// See usage with below example:
/// ```rust
-/// use apache_avro::{serde_avro_bytes, serde_avro_fixed};
+/// use apache_avro::{AvroSchema, serde_avro_bytes, serde_avro_fixed};
/// use serde::{Deserialize, Serialize};
///
-/// #[derive(Serialize, Deserialize)]
+/// #[derive(AvroSchema, Serialize, Deserialize)]
/// struct StructWithBytes {
+/// #[avro(with)]
/// #[serde(with = "serde_avro_bytes")]
/// vec_field: Vec<u8>,
///
+/// #[avro(with = serde_avro_fixed::get_schema_in_ctxt::<6>)]
/// #[serde(with = "serde_avro_fixed")]
/// fixed_field: [u8; 6],
/// }
@@ -59,6 +61,16 @@ pub(crate) enum BytesType {
pub mod serde_avro_bytes {
use serde::{Deserializer, Serializer};
+ use crate::{
+ Schema,
+ schema::{Names, Namespace},
+ };
+
+ /// Returns [`Schema::Bytes`]
+ pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace:
&Namespace) -> Schema {
+ Schema::Bytes
+ }
+
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -81,14 +93,16 @@ pub mod serde_avro_bytes {
///
/// See usage with below example:
/// ```rust
-/// use apache_avro::{serde_avro_bytes_opt, serde_avro_fixed_opt};
+/// use apache_avro::{AvroSchema, serde_avro_bytes_opt, serde_avro_fixed_opt};
/// use serde::{Deserialize, Serialize};
///
-/// #[derive(Serialize, Deserialize)]
+/// #[derive(AvroSchema, Serialize, Deserialize)]
/// struct StructWithBytes {
+/// #[avro(with)]
/// #[serde(with = "serde_avro_bytes_opt")]
/// vec_field: Option<Vec<u8>>,
///
+/// #[avro(with = serde_avro_fixed_opt::get_schema_in_ctxt::<6>)]
/// #[serde(with = "serde_avro_fixed_opt")]
/// fixed_field: Option<[u8; 6]>,
/// }
@@ -97,6 +111,18 @@ pub mod serde_avro_bytes_opt {
use serde::{Deserializer, Serializer};
use std::borrow::Borrow;
+ use crate::{
+ Schema,
+ schema::{Names, Namespace, UnionSchema},
+ };
+
+ /// Returns `Schema::Union(Schema::Null, Schema::Bytes)`
+ pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace:
&Namespace) -> Schema {
+ Schema::Union(
+ UnionSchema::new(vec![Schema::Null, Schema::Bytes]).expect("This
is a valid union"),
+ )
+ }
+
pub fn serialize<S, B>(bytes: &Option<B>, serializer: S) -> Result<S::Ok,
S::Error>
where
S: Serializer,
@@ -120,14 +146,16 @@ pub mod serde_avro_bytes_opt {
///
/// See usage with below example:
/// ```rust
-/// use apache_avro::{serde_avro_bytes, serde_avro_fixed};
+/// use apache_avro::{AvroSchema, serde_avro_bytes, serde_avro_fixed};
/// use serde::{Deserialize, Serialize};
///
-/// #[derive(Serialize, Deserialize)]
+/// #[derive(AvroSchema, Serialize, Deserialize)]
/// struct StructWithBytes {
+/// #[avro(with)]
/// #[serde(with = "serde_avro_bytes")]
/// vec_field: Vec<u8>,
///
+/// #[avro(with = serde_avro_fixed::get_schema_in_ctxt::<6>)]
/// #[serde(with = "serde_avro_fixed")]
/// fixed_field: [u8; 6],
/// }
@@ -136,6 +164,29 @@ pub mod serde_avro_fixed {
use super::{BytesType, SER_BYTES_TYPE};
use serde::{Deserializer, Serializer};
+ use crate::{
+ Schema,
+ schema::{FixedSchema, Name, Names, Namespace},
+ };
+
+ /// Returns `Schema::Fixed(N)` named `serde_avro_fixed_{N}`
+ #[expect(clippy::map_entry, reason = "We don't use the value from the
map")]
+ pub fn get_schema_in_ctxt<const N: usize>(
+ named_schemas: &mut Names,
+ enclosing_namespace: &Namespace,
+ ) -> Schema {
+ let name = Name::new(&format!("serde_avro_fixed_{N}"))
+ .expect("Name is valid")
+ .fully_qualified_name(enclosing_namespace);
+ if named_schemas.contains_key(&name) {
+ Schema::Ref { name }
+ } else {
+ let schema =
Schema::Fixed(FixedSchema::builder().name(name.clone()).size(N).build());
+ named_schemas.insert(name, schema.clone());
+ schema
+ }
+ }
+
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -161,14 +212,16 @@ pub mod serde_avro_fixed {
///
/// See usage with below example:
/// ```rust
-/// use apache_avro::{serde_avro_bytes_opt, serde_avro_fixed_opt};
+/// use apache_avro::{AvroSchema, serde_avro_bytes_opt, serde_avro_fixed_opt};
/// use serde::{Deserialize, Serialize};
///
-/// #[derive(Serialize, Deserialize)]
+/// #[derive(AvroSchema, Serialize, Deserialize)]
/// struct StructWithBytes {
+/// #[avro(with)]
/// #[serde(with = "serde_avro_bytes_opt")]
/// vec_field: Option<Vec<u8>>,
///
+/// #[avro(with = serde_avro_fixed_opt::get_schema_in_ctxt::<6>)]
/// #[serde(with = "serde_avro_fixed_opt")]
/// fixed_field: Option<[u8; 6]>,
/// }
@@ -178,6 +231,28 @@ pub mod serde_avro_fixed_opt {
use serde::{Deserializer, Serializer};
use std::borrow::Borrow;
+ use crate::{
+ Schema,
+ schema::{Names, Namespace, UnionSchema},
+ };
+
+ /// Returns `Schema::Union(Schema::Null, Schema::Fixed(N))` where the
fixed schema is named `serde_avro_fixed_{N}`
+ pub fn get_schema_in_ctxt<const N: usize>(
+ named_schemas: &mut Names,
+ enclosing_namespace: &Namespace,
+ ) -> Schema {
+ Schema::Union(
+ UnionSchema::new(vec![
+ Schema::Null,
+ super::serde_avro_fixed::get_schema_in_ctxt::<N>(
+ named_schemas,
+ enclosing_namespace,
+ ),
+ ])
+ .expect("This is a valid union"),
+ )
+ }
+
pub fn serialize<S, B>(bytes: &Option<B>, serializer: S) -> Result<S::Ok,
S::Error>
where
S: Serializer,
@@ -209,11 +284,12 @@ pub mod serde_avro_fixed_opt {
///
/// See usage with below example:
/// ```rust
-/// use apache_avro::serde_avro_slice;
+/// use apache_avro::{AvroSchema, serde_avro_slice};
/// use serde::{Deserialize, Serialize};
///
-/// #[derive(Serialize, Deserialize)]
+/// #[derive(AvroSchema, Serialize, Deserialize)]
/// struct StructWithBytes<'a> {
+/// #[avro(with)]
/// #[serde(with = "serde_avro_slice")]
/// slice_field: &'a [u8],
/// }
@@ -222,6 +298,16 @@ pub mod serde_avro_slice {
use super::DE_BYTES_BORROWED;
use serde::{Deserializer, Serializer};
+ use crate::{
+ Schema,
+ schema::{Names, Namespace},
+ };
+
+ /// Returns [`Schema::Bytes`]
+ pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace:
&Namespace) -> Schema {
+ Schema::Bytes
+ }
+
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -252,11 +338,12 @@ pub mod serde_avro_slice {
///
/// See usage with below example:
/// ```rust
-/// use apache_avro::serde_avro_slice_opt;
+/// use apache_avro::{AvroSchema, serde_avro_slice_opt};
/// use serde::{Deserialize, Serialize};
///
-/// #[derive(Serialize, Deserialize)]
+/// #[derive(AvroSchema, Serialize, Deserialize)]
/// struct StructWithBytes<'a> {
+/// #[avro(with)]
/// #[serde(with = "serde_avro_slice_opt")]
/// slice_field: Option<&'a [u8]>,
/// }
@@ -266,6 +353,18 @@ pub mod serde_avro_slice_opt {
use serde::{Deserializer, Serializer};
use std::borrow::Borrow;
+ use crate::{
+ Schema,
+ schema::{Names, Namespace, UnionSchema},
+ };
+
+ /// Returns `Schema::Union(Schema::Null, Schema::Bytes)`
+ pub fn get_schema_in_ctxt(_names: &mut Names, _enclosing_namespace:
&Namespace) -> Schema {
+ Schema::Union(
+ UnionSchema::new(vec![Schema::Null, Schema::Bytes]).expect("This
is a valid union"),
+ )
+ }
+
pub fn serialize<S, B>(bytes: &Option<B>, serializer: S) -> Result<S::Ok,
S::Error>
where
S: Serializer,
diff --git a/avro/src/schema.rs b/avro/src/schema.rs
index ce75832..e56eab8 100644
--- a/avro/src/schema.rs
+++ b/avro/src/schema.rs
@@ -592,7 +592,8 @@ pub(crate) fn resolve_names(
| Schema::Decimal(DecimalSchema {
inner: InnerDecimalSchema::Fixed(FixedSchema { name, .. }),
..
- }) => {
+ })
+ | Schema::Duration(FixedSchema { name, .. }) => {
let fully_qualified_name =
name.fully_qualified_name(enclosing_namespace);
if names
.insert(fully_qualified_name.clone(), schema.clone())
@@ -2000,13 +2001,13 @@ impl Parser {
let symbols: Vec<String> = symbols_opt
.and_then(|v| v.as_array())
- .ok_or(Details::GetEnumSymbolsField)
+ .ok_or_else(|| Error::from(Details::GetEnumSymbolsField))
.and_then(|symbols| {
symbols
.iter()
.map(|symbol| symbol.as_str().map(|s| s.to_string()))
.collect::<Option<_>>()
- .ok_or(Details::GetEnumSymbols)
+ .ok_or_else(|| Error::from(Details::GetEnumSymbols))
})?;
let mut existing_symbols: HashSet<&String> =
HashSet::with_capacity(symbols.len());
@@ -2611,6 +2612,7 @@ pub trait AvroSchema {
/// }
///}
/// ```
+///
/// ### Passthrough implementation
///
/// To construct a schema for a Type that acts as in "inner" type, such as for
smart pointers, simply
@@ -2622,7 +2624,9 @@ pub trait AvroSchema {
/// }
///}
/// ```
-///### Complex implementation
+///
+/// ### Complex implementation
+///
/// To implement this for Named schema there is a general form needed to avoid
creating invalid
/// schemas or infinite loops.
/// ```ignore
@@ -2679,7 +2683,6 @@ impl_schema!(f32, Schema::Float);
impl_schema!(f64, Schema::Double);
impl_schema!(String, Schema::String);
impl_schema!(str, Schema::String);
-impl_schema!(uuid::Uuid, Schema::Uuid(UuidSchema::String));
impl<T> AvroSchemaComponent for &T
where
@@ -2788,22 +2791,54 @@ where
}
impl AvroSchemaComponent for core::time::Duration {
+ /// The schema is [`Schema::Duration`] with the name `duration`.
+ ///
+ /// This is a lossy conversion as this Avro type does not store the amount
of nanoseconds.
+ #[expect(clippy::map_entry, reason = "We don't use the value from the
map")]
fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace:
&Namespace) -> Schema {
- let name = Name {
- name: "duration".to_string(),
- namespace: enclosing_namespace.clone(),
- };
- named_schemas
- .entry(name.clone())
- .or_insert(Schema::Duration(FixedSchema {
- name,
+ let name = Name::new("duration")
+ .expect("Name is valid")
+ .fully_qualified_name(enclosing_namespace);
+ if named_schemas.contains_key(&name) {
+ Schema::Ref { name }
+ } else {
+ let schema = Schema::Duration(FixedSchema {
+ name: name.clone(),
aliases: None,
doc: None,
size: 12,
default: None,
attributes: Default::default(),
- }))
- .clone()
+ });
+ named_schemas.insert(name, schema.clone());
+ schema
+ }
+ }
+}
+
+impl AvroSchemaComponent for uuid::Uuid {
+ /// The schema is [`Schema::Uuid`] with the name `uuid`.
+ ///
+ /// The underlying schema is [`Schema::Fixed`] with a size of 16.
+ #[expect(clippy::map_entry, reason = "We don't use the value from the
map")]
+ fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace:
&Namespace) -> Schema {
+ let name = Name::new("uuid")
+ .expect("Name is valid")
+ .fully_qualified_name(enclosing_namespace);
+ if named_schemas.contains_key(&name) {
+ Schema::Ref { name }
+ } else {
+ let schema = Schema::Uuid(UuidSchema::Fixed(FixedSchema {
+ name: name.clone(),
+ aliases: None,
+ doc: None,
+ size: 16,
+ default: None,
+ attributes: Default::default(),
+ }));
+ named_schemas.insert(name, schema.clone());
+ schema
+ }
}
}
diff --git a/avro_derive/Cargo.toml b/avro_derive/Cargo.toml
index 6c01ed7..4f6f835 100644
--- a/avro_derive/Cargo.toml
+++ b/avro_derive/Cargo.toml
@@ -37,6 +37,7 @@ proc-macro2 = { default-features = false, version = "1.0.105"
}
quote = { default-features = false, version = "1.0.43" }
serde_json = { workspace = true }
syn = { default-features = false, version = "2.0.114", features = ["full",
"fold"] }
+uuid = { workspace = true }
[dev-dependencies]
apache-avro = { default-features = false, path = "../avro", features =
["derive"] }
diff --git a/avro_derive/src/attributes/avro.rs
b/avro_derive/src/attributes/avro.rs
index ceeafe5..ea171b5 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 a field.
+#[derive(Debug, FromMeta, PartialEq, Default)]
+#[darling(from_expr = |expr| Ok(With::Expr(expr.clone())))]
+pub enum With {
+ /// Use `<T as AvroSchemaComponent>::get_schema_in_ctxt`.
+ #[default]
+ #[darling(skip)]
+ Trait,
+ /// Use `module::get_schema_in_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 a 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 = "some_module")]` attribute.
+ ///
+ /// When it's provided with an argument (`#[avro(with = some_fn)]`), 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..5657179 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,47 @@ 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_in_ctxt`.
+ #[default]
+ Trait,
+ /// Use `module::get_schema_in_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).map_err(|err| {
+ syn::Error::new(
+ span,
+ format!("Expected a path for `#[serde(with =
\"..\")]`: {err:?}"),
+ )
+ })?;
+ Ok(Self::Serde(path))
+ } else {
+ Err(syn::Error::new(
+ span,
+ "`#[avro(with)]` requires `#[serde(with =
\"some_module\")]` or provide a function to call `#[avro(with = some_fn)]`",
+ ))
+ }
+ }
+ avro::With::Expr(expr) => Ok(Self::Expr(expr.clone())),
+ }
+ }
+}
+
pub struct FieldOptions {
pub doc: Option<String>,
pub default: Option<String>,
@@ -152,6 +194,7 @@ pub struct FieldOptions {
pub rename: Option<String>,
pub skip: bool,
pub flatten: bool,
+ pub with: With,
}
impl FieldOptions {
@@ -213,6 +256,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 +276,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 9d9490f..b976756 100644
--- a/avro_derive/src/lib.rs
+++ b/avro_derive/src/lib.rs
@@ -23,11 +23,11 @@ mod case;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
- AttrStyle, Attribute, DeriveInput, Ident, Meta, Type, parse_macro_input,
spanned::Spanned,
+ AttrStyle, Attribute, DeriveInput, Expr, Ident, Meta, Type,
parse_macro_input, spanned::Spanned,
};
use crate::{
- attributes::{FieldOptions, NamedTypeOptions, VariantOptions},
+ attributes::{FieldOptions, NamedTypeOptions, VariantOptions, With},
case::RenameRule,
};
@@ -85,13 +85,17 @@ fn derive_avro_schema(input: &mut DeriveInput) ->
Result<TokenStream, Vec<syn::E
#[automatically_derived]
impl #impl_generics apache_avro::AvroSchemaComponent for #ident
#ty_generics #where_clause {
fn get_schema_in_ctxt(named_schemas: &mut
std::collections::HashMap<apache_avro::schema::Name,
apache_avro::schema::Schema>, enclosing_namespace: &Option<String>) ->
apache_avro::schema::Schema {
- let name =
apache_avro::schema::Name::new(#full_schema_name).expect(&format!("Unable to
parse schema name {}",
#full_schema_name)[..]).fully_qualified_name(enclosing_namespace);
- let enclosing_namespace = &name.namespace;
+ let name =
apache_avro::schema::Name::new(#full_schema_name).expect(concat!("Unable to
parse schema name ",
#full_schema_name)).fully_qualified_name(enclosing_namespace);
if named_schemas.contains_key(&name) {
- apache_avro::schema::Schema::Ref{name: name.clone()}
+ apache_avro::schema::Schema::Ref{name}
} else {
+ let enclosing_namespace = &name.namespace;
+ // This is needed because otherwise recursive types will
recurse forever and cause a stack overflow
+ // TODO: Breaking change to AvroSchemaComponent, have
named_schemas be a set instead
named_schemas.insert(name.clone(),
apache_avro::schema::Schema::Ref{name: name.clone()});
- #schema_def
+ let schema = #schema_def;
+ named_schemas.insert(name, schema.clone());
+ schema
}
}
}
@@ -166,7 +170,31 @@ 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::Closure(closure)) => {
+ if closure.inputs.is_empty() {
+ quote! { (#closure)() }
+ } else {
+ return Err(vec![syn::Error::new(
+ field.span(),
+ "Expected closure with 0 parameters",
+ )]);
+ }
+ }
+ With::Expr(Expr::Path(path)) => {
+ quote! { #path(named_schemas, enclosing_namespace) }
+ }
+ With::Expr(_expr) => {
+ return Err(vec![syn::Error::new(
+ field.span(),
+ "Invalid expression, expected function or closure",
+ )]);
+ }
+ };
record_field_exprs.push(quote! {
schema_fields.push(::apache_avro::schema::RecordField {
name: #name.to_string(),
@@ -200,23 +228,25 @@ fn get_data_struct_schema_def(
// the most common case where there is no flatten.
let minimum_fields = record_field_exprs.len();
Ok(quote! {
- let mut schema_fields = 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()
- .map(|field| (field.name.to_owned(), field.position))
- .collect();
- apache_avro::schema::Schema::Record(apache_avro::schema::RecordSchema {
- name,
- aliases: #record_aliases,
- doc: #record_doc,
- fields: schema_fields,
- lookup,
- attributes: Default::default(),
- })
+ {
+ let mut schema_fields = 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()
+ .map(|field| (field.name.to_owned(), field.position))
+ .collect();
+
apache_avro::schema::Schema::Record(apache_avro::schema::RecordSchema {
+ name,
+ aliases: #record_aliases,
+ doc: #record_doc,
+ fields: schema_fields,
+ lookup,
+ attributes: Default::default(),
+ })
+ }
})
}
@@ -362,6 +392,8 @@ fn preserve_vec(op: Vec<impl quote::ToTokens>) ->
TokenStream {
#[cfg(test)]
mod tests {
use super::*;
+ use pretty_assertions::assert_eq;
+
#[test]
fn basic_case() {
let test_struct = quote! {
@@ -476,16 +508,14 @@ mod tests {
enclosing_namespace: &Option<String>
) -> apache_avro::schema::Schema {
let name = apache_avro::schema::Name::new("Basic")
- .expect(&format!("Unable to parse schema name
{}", "Basic")[..])
+ .expect(concat!("Unable to parse schema name
", "Basic"))
.fully_qualified_name(enclosing_namespace);
- let enclosing_namespace = &name.namespace;
if named_schemas.contains_key(&name) {
- apache_avro::schema::Schema::Ref { name:
name.clone() }
+ apache_avro::schema::Schema::Ref { name }
} else {
- named_schemas.insert(
- name.clone(),
- apache_avro::schema::Schema::Ref { name:
name.clone() }
- );
+ let enclosing_namespace = &name.namespace;
+ named_schemas.insert(name.clone(),
apache_avro::schema::Schema::Ref{name: name.clone()});
+ let schema =
apache_avro::schema::Schema::Enum(apache_avro::schema::EnumSchema {
name:
apache_avro::schema::Name::new("Basic").expect(
&format!("Unable to parse enum name
for schema {}", "Basic")[..]
@@ -500,7 +530,9 @@ mod tests {
],
default: Some("A".into()),
attributes: Default::default(),
- })
+ });
+ named_schemas.insert(name, schema.clone());
+ schema
}
}
}
@@ -629,9 +661,9 @@ mod tests {
match syn::parse2::<DeriveInput>(test_struct) {
Ok(mut input) => {
let schema_res = derive_avro_schema(&mut input);
- let expected_token_stream = r#"let mut schema_fields = Vec ::
with_capacity (1usize) ; schema_fields . push (:: apache_avro :: schema ::
RecordField { name : "a3" . to_string () , doc : Some ("a doc" . into ()) ,
default : Some (serde_json :: from_str ("123") . expect (format ! ("Invalid
JSON: {:?}" , "123") . as_str ())) , aliases : Some (vec ! ["a1" . into () ,
"a2" . into ()]) , schema : < i32 as apache_avro :: AvroSchemaComponent > ::
get_schema_in_ctxt (named_schemas [...]
+ let expected_token_stream = r#"# [automatically_derived] impl
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name ,
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >)
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name
:: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) .
fully_qualified_name (enclosing [...]
let schema_token_stream = schema_res.unwrap().to_string();
- assert!(schema_token_stream.contains(expected_token_stream));
+ assert_eq!(schema_token_stream, expected_token_stream);
}
Err(error) => panic!(
"Failed to parse as derive input when it should be able to.
Error: {error:?}"
@@ -648,9 +680,9 @@ mod tests {
match syn::parse2::<DeriveInput>(test_enum) {
Ok(mut input) => {
let schema_res = derive_avro_schema(&mut input);
- let expected_token_stream = r#"let name = apache_avro ::
schema :: Name :: new ("A") . expect (& format ! ("Unable to parse schema name
{}" , "A") [..]) . fully_qualified_name (enclosing_namespace) ; let
enclosing_namespace = & name . namespace ; if named_schemas . contains_key (&
name) { apache_avro :: schema :: Schema :: Ref { name : name . clone () } }
else { named_schemas . insert (name . clone () , apache_avro :: schema ::
Schema :: Ref { name : name . clone () }) ; [...]
+ let expected_token_stream = r#"# [automatically_derived] impl
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name ,
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >)
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name
:: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) .
fully_qualified_name (enclosing [...]
let schema_token_stream = schema_res.unwrap().to_string();
- assert!(schema_token_stream.contains(expected_token_stream));
+ assert_eq!(schema_token_stream, expected_token_stream);
}
Err(error) => panic!(
"Failed to parse as derive input when it should be able to.
Error: {error:?}"
@@ -671,9 +703,9 @@ mod tests {
match syn::parse2::<DeriveInput>(test_struct) {
Ok(mut input) => {
let schema_res = derive_avro_schema(&mut input);
- let expected_token_stream = r#"let name = apache_avro ::
schema :: Name :: new ("A") . expect (& format ! ("Unable to parse schema name
{}" , "A") [..]) . fully_qualified_name (enclosing_namespace) ; let
enclosing_namespace = & name . namespace ; if named_schemas . contains_key (&
name) { apache_avro :: schema :: Schema :: Ref { name : name . clone () } }
else { named_schemas . insert (name . clone () , apache_avro :: schema ::
Schema :: Ref { name : name . clone () }) ; [...]
+ let expected_token_stream = r#"# [automatically_derived] impl
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name ,
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >)
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name
:: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) .
fully_qualified_name (enclosing [...]
let schema_token_stream = schema_res.unwrap().to_string();
- assert!(schema_token_stream.contains(expected_token_stream));
+ assert_eq!(schema_token_stream, expected_token_stream);
}
Err(error) => panic!(
"Failed to parse as derive input when it should be able to.
Error: {error:?}"
@@ -691,9 +723,9 @@ mod tests {
match syn::parse2::<DeriveInput>(test_enum) {
Ok(mut input) => {
let schema_res = derive_avro_schema(&mut input);
- let expected_token_stream = r#"let name = apache_avro ::
schema :: Name :: new ("B") . expect (& format ! ("Unable to parse schema name
{}" , "B") [..]) . fully_qualified_name (enclosing_namespace) ; let
enclosing_namespace = & name . namespace ; if named_schemas . contains_key (&
name) { apache_avro :: schema :: Schema :: Ref { name : name . clone () } }
else { named_schemas . insert (name . clone () , apache_avro :: schema ::
Schema :: Ref { name : name . clone () }) ; [...]
+ let expected_token_stream = r#"# [automatically_derived] impl
apache_avro :: AvroSchemaComponent for B { fn get_schema_in_ctxt (named_schemas
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name ,
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >)
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name
:: new ("B") . expect (concat ! ("Unable to parse schema name " , "B")) .
fully_qualified_name (enclosing [...]
let schema_token_stream = schema_res.unwrap().to_string();
- assert!(schema_token_stream.contains(expected_token_stream));
+ assert_eq!(schema_token_stream, expected_token_stream);
}
Err(error) => panic!(
"Failed to parse as derive input when it should be able to.
Error: {error:?}"
@@ -715,9 +747,9 @@ mod tests {
match syn::parse2::<DeriveInput>(test_struct) {
Ok(mut input) => {
let schema_res = derive_avro_schema(&mut input);
- let expected_token_stream = r#"let name = apache_avro ::
schema :: Name :: new ("A") . expect (& format ! ("Unable to parse schema name
{}" , "A") [..]) . fully_qualified_name (enclosing_namespace) ; let
enclosing_namespace = & name . namespace ; if named_schemas . contains_key (&
name) { apache_avro :: schema :: Schema :: Ref { name : name . clone () } }
else { named_schemas . insert (name . clone () , apache_avro :: schema ::
Schema :: Ref { name : name . clone () }) ; [...]
+ let expected_token_stream = r#"# [automatically_derived] impl
apache_avro :: AvroSchemaComponent for A { fn get_schema_in_ctxt (named_schemas
: & mut std :: collections :: HashMap < apache_avro :: schema :: Name ,
apache_avro :: schema :: Schema > , enclosing_namespace : & Option < String >)
-> apache_avro :: schema :: Schema { let name = apache_avro :: schema :: Name
:: new ("A") . expect (concat ! ("Unable to parse schema name " , "A")) .
fully_qualified_name (enclosing [...]
let schema_token_stream = schema_res.unwrap().to_string();
- assert!(schema_token_stream.contains(expected_token_stream));
+ assert_eq!(schema_token_stream, expected_token_stream);
}
Err(error) => panic!(
"Failed to parse as derive input when it should be able to.
Error: {error:?}"
diff --git a/avro_derive/tests/derive.rs b/avro_derive/tests/derive.rs
index 0d4df09..758774e 100644
--- a/avro_derive/tests/derive.rs
+++ b/avro_derive/tests/derive.rs
@@ -17,15 +17,15 @@
use apache_avro::{
Reader, Schema, Writer, from_value,
- schema::{AvroSchema, AvroSchemaComponent},
+ schema::{
+ Alias, AvroSchema, AvroSchemaComponent, EnumSchema, FixedSchema, Name,
Names, Namespace,
+ RecordSchema,
+ },
};
use apache_avro_derive::*;
use proptest::prelude::*;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
-use std::collections::HashMap;
-
-use apache_avro::schema::{Alias, EnumSchema, RecordSchema};
-use std::{borrow::Cow, sync::Mutex};
+use std::{borrow::Cow, collections::HashMap, sync::Mutex};
use pretty_assertions::assert_eq;
@@ -1828,6 +1828,174 @@ fn avro_rs_247_serde_flatten_support_with_skip() {
});
}
+#[test]
+fn avro_rs_397_with() {
+ let schema = Schema::parse_str(
+ r#"
+ {
+ "type":"record",
+ "name":"Foo",
+ "fields": [
+ {
+ "name":"a",
+ "type":"bytes"
+ },
+ {
+ "name":"b",
+ "type":"long"
+ },
+ {
+ "name":"c",
+ "type":"bytes"
+ }
+ ]
+ }
+ "#,
+ )
+ .unwrap();
+
+ fn long_schema(_named_schemas: &mut Names, _enclosing_namespace:
&Namespace) -> Schema {
+ Schema::Long
+ }
+
+ mod module {
+ use super::*;
+ pub fn get_schema_in_ctxt(
+ _named_schemas: &mut Names,
+ _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,
+ #[avro(with = module::get_schema_in_ctxt)]
+ c: String,
+ }
+
+ assert_eq!(schema, Foo::get_schema());
+}
+
+#[test]
+fn avro_rs_397_with_generic() {
+ let schema = Schema::parse_str(
+ r#"
+ {
+ "type":"record",
+ "name":"Foo",
+ "fields": [
+ {
+ "name":"a",
+ "type": {
+ "type": "fixed",
+ "size": 15,
+ "name": "fixed_15"
+ }
+ }
+ ]
+ }
+ "#,
+ )
+ .unwrap();
+
+ fn generic<const N: usize>(
+ _named_schemas: &mut Names,
+ _enclosing_namespace: &Namespace,
+ ) -> Schema {
+ Schema::Fixed(FixedSchema {
+ name: Name::new(&format!("fixed_{N}")).unwrap(),
+ aliases: None,
+ doc: None,
+ size: N,
+ default: None,
+ attributes: Default::default(),
+ })
+ }
+
+ #[allow(dead_code)]
+ #[derive(AvroSchema)]
+ struct Foo {
+ #[avro(with = generic::<15>)]
+ a: [u8; 15],
+ }
+
+ assert_eq!(schema, Foo::get_schema());
+}
+
+#[test]
+fn avro_rs_397_uuid() {
+ let schema = Schema::parse_str(
+ r#"
+ {
+ "type":"record",
+ "name":"Foo",
+ "fields": [
+ {
+ "name":"baz",
+ "type":{
+ "type":"fixed",
+ "logicalType":"uuid",
+ "name":"uuid",
+ "size":16
+ }
+ }
+ ]
+ }
+ "#,
+ )
+ .unwrap();
+
+ #[derive(AvroSchema, Debug, Clone, PartialEq, Serialize, Deserialize)]
+ struct Foo {
+ #[serde(rename = "baz")]
+ bar: uuid::Uuid,
+ }
+
+ assert_eq!(schema, Foo::get_schema());
+ serde_assert(Foo {
+ bar: uuid::Uuid::nil(),
+ });
+}
+
+#[test]
+fn avro_rs_397_derive_with_expr_lambda() {
+ let schema = r#"
+ {
+ "type":"record",
+ "name":"Foo",
+ "fields": [
+ {
+ "name": "_a",
+ "type": "bytes"
+ },
+ {
+ "name": "_b",
+ "type": "int"
+ }
+ ]
+ }"#;
+
+ let expected_schema = Schema::parse_str(schema).unwrap();
+
+ #[derive(AvroSchema)]
+ struct Foo {
+ #[avro(with = || Schema::Bytes)]
+ _a: String,
+ _b: i32,
+ }
+
+ let derived_schema = Foo::get_schema();
+
+ assert_eq!(expected_schema, derived_schema);
+}
+
#[test]
fn avro_rs_401_do_not_match_typename() {
#[expect(nonstandard_style, reason = "It needs to be exactly this")]
diff --git a/avro_derive/tests/serde.rs b/avro_derive/tests/serde.rs
index f42e97b..5283b5e 100644
--- a/avro_derive/tests/serde.rs
+++ b/avro_derive/tests/serde.rs
@@ -504,4 +504,74 @@ mod field_attributes {
b: 321,
});
}
+
+ #[test]
+ fn avro_rs_397_avroschema_with_bytes() {
+ use apache_avro::{
+ serde_avro_bytes, serde_avro_bytes_opt, serde_avro_fixed,
serde_avro_fixed_opt,
+ serde_avro_slice, serde_avro_slice_opt,
+ };
+
+ #[expect(dead_code, reason = "We only care about the schema")]
+ #[derive(AvroSchema)]
+ struct TestStructWithBytes<'a> {
+ #[avro(with)]
+ #[serde(with = "serde_avro_bytes")]
+ vec_field: Vec<u8>,
+ #[avro(with)]
+ #[serde(with = "serde_avro_bytes_opt")]
+ vec_field_opt: Option<Vec<u8>>,
+
+ #[avro(with = serde_avro_fixed::get_schema_in_ctxt::<6>)]
+ #[serde(with = "serde_avro_fixed")]
+ fixed_field: [u8; 6],
+ #[avro(with = serde_avro_fixed_opt::get_schema_in_ctxt::<7>)]
+ #[serde(with = "serde_avro_fixed_opt")]
+ fixed_field_opt: Option<[u8; 7]>,
+
+ #[avro(with)]
+ #[serde(with = "serde_avro_slice")]
+ slice_field: &'a [u8],
+ #[avro(with)]
+ #[serde(with = "serde_avro_slice_opt")]
+ slice_field_opt: Option<&'a [u8]>,
+ }
+
+ let schema = Schema::parse_str(
+ r#"
+ {
+ "type": "record",
+ "name": "TestStructWithBytes",
+ "fields": [ {
+ "name": "vec_field",
+ "type": "bytes"
+ }, {
+ "name": "vec_field_opt",
+ "type": ["null", "bytes"]
+ }, {
+ "name": "fixed_field",
+ "type": {
+ "name": "serde_avro_fixed_6",
+ "type": "fixed",
+ "size": 6
+ }
+ }, {
+ "name": "fixed_field_opt",
+ "type": ["null", {
+ "name": "serde_avro_fixed_7",
+ "type": "fixed",
+ "size": 7
+ } ]
+ }, {
+ "name": "slice_field",
+ "type": "bytes"
+ }, {
+ "name": "slice_field_opt",
+ "type": ["null", "bytes"]
+ } ]
+ }"#,
+ )
+ .unwrap();
+ assert_eq!(schema, TestStructWithBytes::get_schema())
+ }
}
diff --git a/avro_derive/tests/ui/avro_rs_397_with_closure_parameters.rs
b/avro_derive/tests/ui/avro_rs_397_with_closure_parameters.rs
new file mode 100644
index 0000000..16c82c4
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_397_with_closure_parameters.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 = |_named_schemas, _enclosing_namespace| Schema::Bytes)]
+ a: String,
+ b: i32,
+}
+
+pub fn main() {}
diff --git a/avro_derive/tests/ui/avro_rs_397_with_closure_parameters.stderr
b/avro_derive/tests/ui/avro_rs_397_with_closure_parameters.stderr
new file mode 100644
index 0000000..f8b7570
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_397_with_closure_parameters.stderr
@@ -0,0 +1,14 @@
+error: Expected closure with 0 parameters
+ --> tests/ui/avro_rs_397_with_closure_parameters.rs:22:5
+ |
+22 | / #[avro(with = |_named_schemas, _enclosing_namespace| Schema::Bytes)]
+23 | | a: String,
+ | |_____________^
+
+warning: unused import: `Schema`
+ --> tests/ui/avro_rs_397_with_closure_parameters.rs:18:31
+ |
+18 | use apache_avro::{AvroSchema, Schema};
+ | ^^^^^^
+ |
+ = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default
diff --git a/avro_derive/tests/ui/avro_rs_397_with_expr_string.rs
b/avro_derive/tests/ui/avro_rs_397_with_expr_string.rs
new file mode 100644
index 0000000..dd24c69
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_397_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_397_with_expr_string.stderr
b/avro_derive/tests/ui/avro_rs_397_with_expr_string.stderr
new file mode 100644
index 0000000..9107607
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_397_with_expr_string.stderr
@@ -0,0 +1,6 @@
+error: Invalid expression, expected function or closure
+ --> tests/ui/avro_rs_397_with_expr_string.rs:22:5
+ |
+22 | / #[avro(with = "Schema::Bytes")]
+23 | | a: String,
+ | |_____________^
diff --git a/avro_derive/tests/ui/avro_rs_397_with_expr_type.rs
b/avro_derive/tests/ui/avro_rs_397_with_expr_type.rs
new file mode 100644
index 0000000..93829a4
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_397_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_397_with_expr_type.stderr
b/avro_derive/tests/ui/avro_rs_397_with_expr_type.stderr
new file mode 100644
index 0000000..5147c91
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_397_with_expr_type.stderr
@@ -0,0 +1,8 @@
+error[E0618]: expected function, found `Schema`
+ --> tests/ui/avro_rs_397_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_397_with_word_without_serde.rs
b/avro_derive/tests/ui/avro_rs_397_with_word_without_serde.rs
new file mode 100644
index 0000000..eb44682
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_397_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_397_with_word_without_serde.stderr
b/avro_derive/tests/ui/avro_rs_397_with_word_without_serde.stderr
new file mode 100644
index 0000000..febd45e
--- /dev/null
+++ b/avro_derive/tests/ui/avro_rs_397_with_word_without_serde.stderr
@@ -0,0 +1,6 @@
+error: `#[avro(with)]` requires `#[serde(with = "some_module")]` or provide a
function to call `#[avro(with = some_fn)]`
+ --> tests/ui/avro_rs_397_with_word_without_serde.rs:22:5
+ |
+22 | / #[avro(with)]
+23 | | a: String,
+ | |_____________^