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 5dc46c0  fix!: Stricter schema parsing (#479)
5dc46c0 is described below

commit 5dc46c04d47c4e2d054951d4229fab7a76bfca39
Author: Kriskras99 <[email protected]>
AuthorDate: Tue Feb 24 07:01:30 2026 +0100

    fix!: Stricter schema parsing (#479)
    
    * fix!: Stricter schema parsing
    
    * feat: Log warning if record field contains a logical type
    
    * fix: Review comments
    
    ---------
    
    Co-authored-by: default <[email protected]>
---
 avro/src/bigdecimal.rs             |  12 +-
 avro/src/error.rs                  |   9 +-
 avro/src/schema/mod.rs             | 273 ++++++++++++++++++-------------------
 avro/src/schema/parser.rs          |  19 +--
 avro/src/schema/record/field.rs    |  39 +++---
 avro/src/schema/record/mod.rs      |   1 -
 avro/src/schema/record/schema.rs   |  10 --
 avro/src/schema_compatibility.rs   |   4 +-
 avro/src/serde/ser_schema.rs       |  10 +-
 avro/src/types.rs                  |   2 +-
 avro/tests/avro-3787.rs            |  34 +++--
 avro/tests/schema.rs               |  12 +-
 avro/tests/validators.rs           |   9 +-
 avro_derive/src/attributes/avro.rs |  20 +--
 avro_derive/src/attributes/mod.rs  |  18 +--
 avro_derive/tests/derive.rs        |  12 +-
 avro_test_helper/src/data.rs       |   4 +-
 17 files changed, 234 insertions(+), 254 deletions(-)

diff --git a/avro/src/bigdecimal.rs b/avro/src/bigdecimal.rs
index 28f395d..b887535 100644
--- a/avro/src/bigdecimal.rs
+++ b/avro/src/bigdecimal.rs
@@ -137,8 +137,10 @@ mod tests {
           "fields": [
             {
               "name": "field_name",
-              "type": "bytes",
-              "logicalType": "big-decimal"
+              "type": {
+                "type": "bytes",
+                "logicalType": "big-decimal"
+              }
             }
           ]
         }
@@ -196,8 +198,10 @@ mod tests {
           "fields": [
             {
               "name": "big_decimal",
-              "type": "bytes",
-              "logicalType": "big-decimal"
+              "type": {
+                "type": "bytes",
+                "logicalType": "big-decimal"
+              }
             }
           ]
         }
diff --git a/avro/src/error.rs b/avro/src/error.rs
index 17c7497..b26e5df 100644
--- a/avro/src/error.rs
+++ b/avro/src/error.rs
@@ -309,8 +309,8 @@ pub enum Details {
     #[error("One union type {0:?} must match the `default`'s value type 
{1:?}")]
     GetDefaultUnion(SchemaKind, ValueKind),
 
-    #[error("`default`'s value type of field {0:?} in {1:?} must be {2:?}")]
-    GetDefaultRecordField(String, String, String),
+    #[error("`default`'s value type of field `{0}` in `{1}` must be a `{2:#}`. 
Got: {3:?}")]
+    GetDefaultRecordField(String, String, String, serde_json::Value),
 
     #[error("JSON number {0} could not be converted into an Avro value as it's 
too large")]
     JsonNumberTooLarge(serde_json::Number),
@@ -405,6 +405,9 @@ pub enum Details {
     #[error("No `type` in complex type")]
     GetComplexTypeField,
 
+    #[error("No `type` in record field")]
+    GetRecordFieldTypeField,
+
     #[error("No `fields` in record")]
     GetRecordFieldsJson,
 
@@ -430,7 +433,7 @@ pub enum Details {
     InvalidNamespace(String, &'static str),
 
     #[error(
-        "Invalid schema: There is no type called '{0}', if you meant to define 
a non-primitive schema, it should be defined inside `type` attribute. Please 
review the specification"
+        "Invalid schema: There is no type called '{0}', if you meant to define 
a non-primitive schema, it should be defined inside `type` attribute."
     )]
     InvalidSchemaRecord(String),
 
diff --git a/avro/src/schema/mod.rs b/avro/src/schema/mod.rs
index b127fd0..e733ea1 100644
--- a/avro/src/schema/mod.rs
+++ b/avro/src/schema/mod.rs
@@ -38,7 +38,7 @@ pub use crate::schema::{
 use crate::{
     AvroResult,
     error::{Details, Error},
-    schema::{parser::Parser, record::RecordSchemaParseLocation},
+    schema::parser::Parser,
     schema_equality,
     types::{self, Value},
 };
@@ -1054,7 +1054,7 @@ fn pcf_array(arr: &[JsonValue], defined_names: &mut 
HashSet<String>) -> String {
 }
 
 fn pcf_string(s: &str) -> String {
-    format!("\"{s}\"")
+    format!(r#""{s}""#)
 }
 
 const RESERVED_FIELDS: &[&str] = &[
@@ -1100,9 +1100,9 @@ mod tests {
 
     #[test]
     fn test_primitive_schema() -> TestResult {
-        assert_eq!(Schema::Null, Schema::parse_str("\"null\"")?);
-        assert_eq!(Schema::Int, Schema::parse_str("\"int\"")?);
-        assert_eq!(Schema::Double, Schema::parse_str("\"double\"")?);
+        assert_eq!(Schema::Null, Schema::parse_str(r#""null""#)?);
+        assert_eq!(Schema::Int, Schema::parse_str(r#""int""#)?);
+        assert_eq!(Schema::Double, Schema::parse_str(r#""double""#)?);
         Ok(())
     }
 
@@ -1325,7 +1325,7 @@ mod tests {
             Ok(_) => unreachable!("Expected an error that the name is already 
defined"),
             Err(e) => assert_eq!(
                 e.to_string(),
-                "Two schemas with the same fullname were given: \"A\""
+                r#"Two schemas with the same fullname were given: "A""#
             ),
         }
 
@@ -1835,9 +1835,12 @@ mod tests {
               "name" : "record",
               "fields" : [
                  {
-                    "type" : "enum",
                     "name" : "enum",
-                    "symbols": ["one", "two", "three"]
+                    "type": {
+                        "name" : "enum",
+                        "type" : "enum",
+                        "symbols": ["one", "two", "three"]
+                    }
                  },
                  { "name" : "next", "type" : "enum" }
              ]
@@ -1881,17 +1884,9 @@ mod tests {
                     doc: None,
                     default: None,
                     aliases: None,
-                    schema: Schema::Enum(EnumSchema {
-                        name: Name {
-                            name: "enum".to_owned(),
-                            namespace: None,
-                        },
-                        aliases: None,
-                        doc: None,
-                        symbols: vec!["one".to_string(), "two".to_string(), 
"three".to_string()],
-                        default: None,
-                        attributes: Default::default(),
-                    }),
+                    schema: Schema::Ref {
+                        name: Name::new("enum")?,
+                    },
                     order: RecordFieldOrder::Ascending,
                     position: 1,
                     custom_attributes: Default::default(),
@@ -1918,9 +1913,12 @@ mod tests {
               "name" : "record",
               "fields" : [
                  {
-                    "type" : "fixed",
-                    "name" : "fixed",
-                    "size": 456
+                    "name": "fixed",
+                    "type": {
+                        "type" : "fixed",
+                        "name" : "fixed",
+                        "size": 456
+                    }
                  },
                  { "name" : "next", "type" : "fixed" }
              ]
@@ -1964,16 +1962,9 @@ mod tests {
                     doc: None,
                     default: None,
                     aliases: None,
-                    schema: Schema::Fixed(FixedSchema {
-                        name: Name {
-                            name: "fixed".to_owned(),
-                            namespace: None,
-                        },
-                        aliases: None,
-                        doc: None,
-                        size: 456,
-                        attributes: Default::default(),
-                    }),
+                    schema: Schema::Ref {
+                        name: Name::new("fixed")?,
+                    },
                     order: RecordFieldOrder::Ascending,
                     position: 1,
                     custom_attributes: Default::default(),
@@ -2138,7 +2129,7 @@ mod tests {
         "fields": [
             {"name": "a", "type": "long", "default": 42},
             {"name": "b", "type": "string"},
-            {"name": "c", "type": "long", "logicalType": "timestamp-micros"}
+            {"name": "c", "type": {"type": "long", "logicalType": 
"timestamp-micros"}}
         ]
     }
 "#;
@@ -3068,11 +3059,13 @@ mod tests {
           "fields": [
             {
                 "name": "decimal",
-                "type": "fixed",
-                "name": "nestedFixed",
-                "size": 8,
-                "logicalType": "decimal",
-                "precision": 4
+                "type": {
+                    "type": "fixed",
+                    "name": "nestedFixed",
+                    "size": 8,
+                    "logicalType": "decimal",
+                    "precision": 4
+                }
             }
           ]
         });
@@ -3772,19 +3765,10 @@ mod tests {
             ]
         }
         "#;
-        let expected = Details::GetDefaultRecordField(
-            "f1".to_string(),
-            "ns.record1".to_string(),
-            r#""int""#.to_string(),
-        )
-        .to_string();
-        let result = Schema::parse_str(schema_str);
-        assert!(result.is_err());
-        let err = result
-            .map_err(|e| e.to_string())
-            .err()
-            .unwrap_or_else(|| "unexpected".to_string());
-        assert_eq!(expected, err);
+        assert_eq!(
+            Schema::parse_str(schema_str).unwrap_err().to_string(),
+            r#"`default`'s value type of field `f1` in `ns.record1` must be a 
`"int"`. Got: String("invalid")"#
+        );
 
         Ok(())
     }
@@ -3814,20 +3798,10 @@ mod tests {
             ]
         }
         "#;
-        let expected = Details::GetDefaultRecordField(
-            "f1".to_string(),
-            "ns.record1".to_string(),
-            
r#"{"name":"ns.record2","type":"record","fields":[{"name":"f1_1","type":"int"}]}"#
-                .to_string(),
-        )
-        .to_string();
-        let result = Schema::parse_str(schema_str);
-        assert!(result.is_err());
-        let err = result
-            .map_err(|e| e.to_string())
-            .err()
-            .unwrap_or_else(|| "unexpected".to_string());
-        assert_eq!(expected, err);
+        assert_eq!(
+            Schema::parse_str(schema_str).unwrap_err().to_string(),
+            r#"`default`'s value type of field `f1` in `ns.record1` must be a 
`{"name":"ns.record2","type":"record","fields":[{"name":"f1_1","type":"int"}]}`.
 Got: String("invalid")"#
+        );
 
         Ok(())
     }
@@ -3852,19 +3826,10 @@ mod tests {
             ]
         }
         "#;
-        let expected = Details::GetDefaultRecordField(
-            "f1".to_string(),
-            "ns.record1".to_string(),
-            
r#"{"name":"ns.enum1","type":"enum","symbols":["a","b","c"]}"#.to_string(),
-        )
-        .to_string();
-        let result = Schema::parse_str(schema_str);
-        assert!(result.is_err());
-        let err = result
-            .map_err(|e| e.to_string())
-            .err()
-            .unwrap_or_else(|| "unexpected".to_string());
-        assert_eq!(expected, err);
+        assert_eq!(
+            Schema::parse_str(schema_str).unwrap_err().to_string(),
+            r#"`default`'s value type of field `f1` in `ns.record1` must be a 
`{"name":"ns.enum1","type":"enum","symbols":["a","b","c"]}`. Got: 
String("invalid")"#
+        );
 
         Ok(())
     }
@@ -3889,19 +3854,10 @@ mod tests {
             ]
         }
         "#;
-        let expected = Details::GetDefaultRecordField(
-            "f1".to_string(),
-            "ns.record1".to_string(),
-            r#"{"name":"ns.fixed1","type":"fixed","size":3}"#.to_string(),
-        )
-        .to_string();
-        let result = Schema::parse_str(schema_str);
-        assert!(result.is_err());
-        let err = result
-            .map_err(|e| e.to_string())
-            .err()
-            .unwrap_or_else(|| "unexpected".to_string());
-        assert_eq!(expected, err);
+        assert_eq!(
+            Schema::parse_str(schema_str).unwrap_err().to_string(),
+            r#"`default`'s value type of field `f1` in `ns.record1` must be a 
`{"name":"ns.fixed1","type":"fixed","size":3}`. Got: Number(100)"#
+        );
 
         Ok(())
     }
@@ -3916,8 +3872,10 @@ mod tests {
             "fields": [
                 {
                     "name": "f1",
-                    "type": "array",
-                    "items": "int",
+                    "type": {
+                        "type": "array",
+                        "items": "int"
+                    },
                     "default": "invalid"
                 }
             ]
@@ -3931,7 +3889,7 @@ mod tests {
             .err()
             .unwrap_or_else(|| "unexpected".to_string());
         assert_eq!(
-            r#"Default value for an array must be an array! Got: "invalid""#,
+            r#"`default`'s value type of field `f1` in `ns.record1` must be a 
`{"type":"array","items":"int"}`. Got: String("invalid")"#,
             err
         );
 
@@ -3948,8 +3906,10 @@ mod tests {
             "fields": [
                 {
                     "name": "f1",
-                    "type": "map",
-                    "values": "string",
+                    "type": {
+                        "type": "map",
+                        "values": "string"
+                    },
                     "default": "invalid"
                 }
             ]
@@ -3963,7 +3923,7 @@ mod tests {
             .err()
             .unwrap_or_else(|| "unexpected".to_string());
         assert_eq!(
-            r#"Default value for a map must be an object! Got: "invalid""#,
+            r#"`default`'s value type of field `f1` in `ns.record1` must be a 
`{"type":"map","values":"string"}`. Got: String("invalid")"#,
             err
         );
 
@@ -3998,19 +3958,10 @@ mod tests {
             ]
         }
         "#;
-        let expected = Details::GetDefaultRecordField(
-            "f2".to_string(),
-            "ns.record1".to_string(),
-            r#""ns.record2""#.to_string(),
-        )
-        .to_string();
-        let result = Schema::parse_str(schema_str);
-        assert!(result.is_err());
-        let err = result
-            .map_err(|e| e.to_string())
-            .err()
-            .unwrap_or_else(|| "unexpected".to_string());
-        assert_eq!(expected, err);
+        assert_eq!(
+            Schema::parse_str(schema_str).unwrap_err().to_string(),
+            r#"`default`'s value type of field `f2` in `ns.record1` must be a 
`{"name":"ns.record2","type":"record","fields":[{"name":"f1_1","type":"int"}]}`.
 Got: Object {"f1_1": Bool(true)}"#
+        );
 
         Ok(())
     }
@@ -4771,7 +4722,7 @@ mod tests {
         "fields": [
             {"name": "a", "type": "long", "default": 42, "doc": "The field a"},
             {"name": "b", "type": "string", "namespace": "test.a"},
-            {"name": "c", "type": "long", "logicalType": "timestamp-micros"}
+            {"name": "c", "type": {"type": "long", "logicalType": 
"timestamp-micros"}}
         ]
     }"#;
 
@@ -4813,7 +4764,7 @@ mod tests {
         assert!(schema.is_err());
         assert_eq!(
             schema.unwrap_err().to_string(),
-            "Invalid schema: There is no type called 'record', if you meant to 
define a non-primitive schema, it should be defined inside `type` attribute. 
Please review the specification"
+            "Invalid schema: There is no type called 'record', if you meant to 
define a non-primitive schema, it should be defined inside `type` attribute."
         );
 
         let valid_schema = r#"
@@ -4854,16 +4805,18 @@ mod tests {
                         "fields": [
                             {
                                 "name": "bar",
-                                "type": "array",
-                                "items": {
-                                    "type": "record",
-                                    "name": "baz",
-                                    "fields": [
-                                        {
-                                            "name": "quux",
-                                            "type": "int"
-                                        }
-                                    ]
+                                "type": {
+                                    "type": "array",
+                                    "items": {
+                                        "type": "record",
+                                        "name": "baz",
+                                        "fields": [
+                                            {
+                                                "name": "quux",
+                                                "type": "int"
+                                            }
+                                        ]
+                                    }
                                 }
                             }
                         ]
@@ -4927,16 +4880,18 @@ mod tests {
                         "fields": [
                             {
                                 "name": "bar",
-                                "type": "map",
-                                "values": {
-                                    "type": "record",
-                                    "name": "baz",
-                                    "fields": [
-                                        {
-                                            "name": "quux",
-                                            "type": "int"
-                                        }
-                                    ]
+                                "type": {
+                                    "type": "map",
+                                    "values": {
+                                        "type": "record",
+                                        "name": "baz",
+                                        "fields": [
+                                            {
+                                                "name": "quux",
+                                                "type": "int"
+                                            }
+                                        ]
+                                    }
                                 }
                             }
                         ]
@@ -5280,15 +5235,19 @@ mod tests {
             "fields": [
                 {
                     "name": "one",
-                    "type": "enum",
-                    "name": "ABC",
-                    "symbols": ["A", "B", "C"]
+                    "type": {
+                        "type": "enum",
+                        "name": "ABC",
+                        "symbols": ["A", "B", "C"]
+                    }
                 },
                 {
                     "name": "two",
-                    "type": "array",
-                    "items": "ABC",
-                    "default": ["A", "B", "C"]
+                    "type": {
+                        "type": "array",
+                        "items": "ABC",
+                        "default": ["A", "B", "C"]
+                    }
                 }
             ]
         }"#,
@@ -5409,15 +5368,19 @@ mod tests {
             "fields": [
                 {
                     "name": "one",
-                    "type": "enum",
-                    "name": "ABC",
-                    "symbols": ["A", "B", "C"]
+                    "type": {
+                        "type": "enum",
+                        "name": "ABC",
+                        "symbols": ["A", "B", "C"]
+                    }
                 },
                 {
                     "name": "two",
-                    "type": "map",
-                    "values": "ABC",
-                    "default": {"foo": "A"}
+                    "type": {
+                        "type": "map",
+                        "values": "ABC",
+                        "default": {"foo": "A"}
+                    }
                 }
             ]
         }"#,
@@ -5437,4 +5400,26 @@ mod tests {
 
         Ok(())
     }
+
+    #[test]
+    fn avro_rs_476_enum_cannot_be_directly_in_field() -> TestResult {
+        let schema_str = r#"{
+            "type": "record",
+            "name": "ExampleEnum",
+            "namespace": "com.schema",
+            "fields": [
+                {
+                "name": "wrong_enum",
+                "type": "enum",
+                "symbols": ["INSERT", "UPDATE"]
+                }
+            ]
+        }"#;
+        let result = Schema::parse_str(schema_str).unwrap_err();
+        assert_eq!(
+            result.to_string(),
+            "Invalid schema: There is no type called 'enum', if you meant to 
define a non-primitive schema, it should be defined inside `type` attribute."
+        );
+        Ok(())
+    }
 }
diff --git a/avro/src/schema/parser.rs b/avro/src/schema/parser.rs
index 3b2acc3..f896e17 100644
--- a/avro/src/schema/parser.rs
+++ b/avro/src/schema/parser.rs
@@ -16,7 +16,6 @@
 // under the License.
 
 use crate::error::Details;
-use crate::schema::record::RecordSchemaParseLocation;
 use crate::schema::{
     Alias, Aliases, ArraySchema, DecimalMetadata, DecimalSchema, EnumSchema, 
FixedSchema,
     MapSchema, Name, Names, Namespace, Precision, RecordField, RecordSchema, 
Scale, Schema,
@@ -110,9 +109,7 @@ impl Parser {
     ) -> AvroResult<Schema> {
         match *value {
             Value::String(ref t) => self.parse_known_schema(t.as_str(), 
enclosing_namespace),
-            Value::Object(ref data) => {
-                self.parse_complex(data, enclosing_namespace, 
RecordSchemaParseLocation::Root)
-            }
+            Value::Object(ref data) => self.parse_complex(data, 
enclosing_namespace),
             Value::Array(ref data) => self.parse_union(data, 
enclosing_namespace),
             _ => Err(Details::ParseSchemaFromValidJson.into()),
         }
@@ -252,7 +249,6 @@ impl Parser {
         &mut self,
         complex: &Map<String, Value>,
         enclosing_namespace: &Namespace,
-        parse_location: RecordSchemaParseLocation,
     ) -> AvroResult<Schema> {
         // Try to parse this as a native complex type.
         fn parse_as_native_complex(
@@ -461,23 +457,14 @@ impl Parser {
         }
         match complex.get("type") {
             Some(Value::String(t)) => match t.as_str() {
-                "record" => match parse_location {
-                    RecordSchemaParseLocation::Root => {
-                        self.parse_record(complex, enclosing_namespace)
-                    }
-                    RecordSchemaParseLocation::FromField => {
-                        self.fetch_schema_ref(t, enclosing_namespace)
-                    }
-                },
+                "record" => self.parse_record(complex, enclosing_namespace),
                 "enum" => self.parse_enum(complex, enclosing_namespace),
                 "array" => self.parse_array(complex, enclosing_namespace),
                 "map" => self.parse_map(complex, enclosing_namespace),
                 "fixed" => self.parse_fixed(complex, enclosing_namespace),
                 other => self.parse_known_schema(other, enclosing_namespace),
             },
-            Some(Value::Object(data)) => {
-                self.parse_complex(data, enclosing_namespace, 
RecordSchemaParseLocation::Root)
-            }
+            Some(Value::Object(data)) => self.parse_complex(data, 
enclosing_namespace),
             Some(Value::Array(variants)) => self.parse_union(variants, 
enclosing_namespace),
             Some(unknown) => 
Err(Details::GetComplexType(unknown.clone()).into()),
             None => Err(Details::GetComplexTypeField.into()),
diff --git a/avro/src/schema/record/field.rs b/avro/src/schema/record/field.rs
index 70a0268..f237f1b 100644
--- a/avro/src/schema/record/field.rs
+++ b/avro/src/schema/record/field.rs
@@ -17,12 +17,11 @@
 
 use crate::AvroResult;
 use crate::error::Details;
-use crate::schema::{
-    Documentation, Name, Names, Parser, RecordSchemaParseLocation, Schema, 
SchemaKind,
-};
+use crate::schema::{Documentation, Name, Names, Parser, Schema, SchemaKind};
 use crate::types;
 use crate::util::MapHelper;
 use crate::validator::validate_record_field_name;
+use log::warn;
 use serde::ser::SerializeMap;
 use serde::{Serialize, Serializer};
 use serde_json::{Map, Value};
@@ -81,12 +80,14 @@ impl RecordField {
 
         validate_record_field_name(&name)?;
 
-        // TODO: "type" = "<record name>"
-        let schema = parser.parse_complex(
-            field,
-            &enclosing_record.namespace,
-            RecordSchemaParseLocation::FromField,
-        )?;
+        let ty = field.get("type").ok_or(Details::GetRecordFieldTypeField)?;
+        let schema = parser.parse(ty, &enclosing_record.namespace)?;
+
+        if let Some(logical_type) = field.get("logicalType") {
+            warn!(
+                "Ignored the {enclosing_record}.logicalType property 
(`{logical_type}`). It should probably be nested inside the `type` for the 
field"
+            );
+        }
 
         let default = field.get("default").cloned();
         Self::resolve_default_value(
@@ -120,7 +121,7 @@ impl RecordField {
             aliases,
             order,
             position,
-            custom_attributes: RecordField::get_field_custom_attributes(field, 
&schema),
+            custom_attributes: RecordField::get_field_custom_attributes(field),
             schema,
         })
     }
@@ -162,10 +163,14 @@ impl RecordField {
                         .is_ok();
 
                     if !resolved {
+                        let schemata = 
names.values().cloned().collect::<Vec<_>>();
                         return Err(Details::GetDefaultRecordField(
                             field_name.to_string(),
                             record_name.to_string(),
-                            field_schema.canonical_form(),
+                            field_schema
+                                .independent_canonical_form(&schemata)
+                                .unwrap_or_else(|_| 
field_schema.canonical_form()),
+                            value.clone(),
                         )
                         .into());
                     }
@@ -176,19 +181,11 @@ impl RecordField {
         Ok(())
     }
 
-    fn get_field_custom_attributes(
-        field: &Map<String, Value>,
-        schema: &Schema,
-    ) -> BTreeMap<String, Value> {
+    fn get_field_custom_attributes(field: &Map<String, Value>) -> 
BTreeMap<String, Value> {
         let mut custom_attributes: BTreeMap<String, Value> = BTreeMap::new();
         for (key, value) in field {
             match key.as_str() {
-                "type" | "name" | "doc" | "default" | "order" | "position" | 
"aliases"
-                | "logicalType" => continue,
-                key if key == "symbols" && matches!(schema, Schema::Enum(_)) 
=> continue,
-                key if key == "size" && matches!(schema, Schema::Fixed(_)) => 
continue,
-                key if key == "items" && matches!(schema, Schema::Array(_)) => 
continue,
-                key if key == "values" && matches!(schema, Schema::Map(_)) => 
continue,
+                "type" | "name" | "doc" | "default" | "order" | "aliases" => 
continue,
                 _ => custom_attributes.insert(key.clone(), value.clone()),
             };
         }
diff --git a/avro/src/schema/record/mod.rs b/avro/src/schema/record/mod.rs
index 0d1c5a1..080d9fe 100644
--- a/avro/src/schema/record/mod.rs
+++ b/avro/src/schema/record/mod.rs
@@ -19,5 +19,4 @@ mod field;
 pub use field::{RecordField, RecordFieldBuilder, RecordFieldOrder};
 
 mod schema;
-pub(crate) use schema::RecordSchemaParseLocation;
 pub use schema::{RecordSchema, RecordSchemaBuilder};
diff --git a/avro/src/schema/record/schema.rs b/avro/src/schema/record/schema.rs
index 297dde9..b14bad7 100644
--- a/avro/src/schema/record/schema.rs
+++ b/avro/src/schema/record/schema.rs
@@ -66,16 +66,6 @@ fn calculate_lookup_table(fields: &[RecordField]) -> 
BTreeMap<String, usize> {
         .collect()
 }
 
-#[derive(Debug, Default)]
-pub(crate) enum RecordSchemaParseLocation {
-    /// When the parse is happening at root level
-    #[default]
-    Root,
-
-    /// When the parse is happening inside a record field
-    FromField,
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/avro/src/schema_compatibility.rs b/avro/src/schema_compatibility.rs
index 20bbef2..4cc8c2f 100644
--- a/avro/src/schema_compatibility.rs
+++ b/avro/src/schema_compatibility.rs
@@ -542,7 +542,7 @@ mod tests {
     }
 
     fn int_list_record_schema() -> Schema {
-        Schema::parse_str(r#"{"type":"record", "name":"List", "fields": 
[{"name": "head", "type": "int"},{"name": "tail", "type": "array", "items": 
"int"}]}"#).unwrap()
+        Schema::parse_str(r#"{"type":"record", "name":"List", "fields": 
[{"name": "head", "type": "int"},{"name": "tail", "type": {"type": "array", 
"items": "int"}}]}"#).unwrap()
     }
 
     fn long_list_record_schema() -> Schema {
@@ -551,7 +551,7 @@ mod tests {
       {
         "type":"record", "name":"List", "fields": [
           {"name": "head", "type": "long"},
-          {"name": "tail", "type": "array", "items": "long"}
+          {"name": "tail", "type": {"type": "array", "items": "long"}}
       ]}
 "#,
         )
diff --git a/avro/src/serde/ser_schema.rs b/avro/src/serde/ser_schema.rs
index 7fb52ab..3945263 100644
--- a/avro/src/serde/ser_schema.rs
+++ b/avro/src/serde/ser_schema.rs
@@ -816,7 +816,7 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
                     }
                 }
                 Err(create_error(format!(
-                    "Cannot find a Fixed(size = 16, name = \"i128\") schema in 
{:?}",
+                    r#"Cannot find a Fixed(size = 16, name = "i128") schema in 
{:?}"#,
                     union_schema.schemas
                 )))
             }
@@ -984,7 +984,7 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
                     }
                 }
                 Err(create_error(format!(
-                    "Cannot find a matching Int-like, Long-like or Fixed(size 
= 8, name \"u64\") schema in {:?}",
+                    r#"Cannot find a matching Int-like, Long-like or 
Fixed(size = 8, name "u64") schema in {:?}"#,
                     union_schema.schemas
                 )))
             }
@@ -1019,7 +1019,7 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
                     }
                 }
                 Err(create_error(format!(
-                    "Cannot find a Fixed(size = 16, name = \"u128\") schema in 
{:?}",
+                    r#"Cannot find a Fixed(size = 16, name = "u128") schema in 
{:?}"#,
                     union_schema.schemas
                 )))
             }
@@ -1133,7 +1133,7 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
                     }
                 }
                 Err(create_error(format!(
-                    "Cannot find a matching String, Bytes or Fixed(size = 4, 
name = \"char\") schema in {union_schema:?}"
+                    r#"Cannot find a matching String, Bytes or Fixed(size = 4, 
name = "char") schema in {union_schema:?}"#
                 )))
             }
             expected => Err(create_error(format!("Expected {expected}. Got: 
char"))),
@@ -3014,7 +3014,7 @@ mod tests {
                 {"name": "stringField", "type": "string"},
                 {"name": "intField", "type": "int"},
                 {"name": "bigDecimalField", "type": {"type": "bytes", 
"logicalType": "big-decimal"}},
-                {"name": "uuidField", "type": "fixed", "size": 16, 
"logicalType": "uuid"},
+                {"name": "uuidField", "type": {"name": "uuid", "type": 
"fixed", "size": 16, "logicalType": "uuid"}},
                 {"name": "innerRecord", "type": ["null", "TestRecord"]}
             ]
         }"#,
diff --git a/avro/src/types.rs b/avro/src/types.rs
index 393f484..6816599 100644
--- a/avro/src/types.rs
+++ b/avro/src/types.rs
@@ -1389,7 +1389,7 @@ mod tests {
                     attributes: BTreeMap::new(),
                 }),
                 false,
-                "Invalid value: Fixed(11, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 
for schema: Duration(FixedSchema { name: Name { name: \"TestName\", namespace: 
None }, aliases: None, doc: None, size: 12, attributes: {} }). Reason: The 
value's size ('11') must be exactly 12 to be a Duration",
+                r#"Invalid value: Fixed(11, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 
10]) for schema: Duration(FixedSchema { name: Name { name: "TestName", 
namespace: None }, aliases: None, doc: None, size: 12, attributes: {} }). 
Reason: The value's size ('11') must be exactly 12 to be a Duration"#,
             ),
             (
                 Value::Record(vec![("unknown_field_name".to_string(), 
Value::Null)]),
diff --git a/avro/tests/avro-3787.rs b/avro/tests/avro-3787.rs
index 1795fca..192c573 100644
--- a/avro/tests/avro-3787.rs
+++ b/avro/tests/avro-3787.rs
@@ -204,15 +204,17 @@ fn 
avro_3787_deserialize_union_with_unknown_symbol_no_ref() -> TestResult {
                             "name": "BarParent",
                             "fields": [
                                 {
-                                    "type": "enum",
                                     "name": "Bar",
-                                    "symbols":
-                                    [
-                                        "bar0",
-                                        "bar1",
-                                        "bar2"
-                                    ],
-                                    "default": "bar0"
+                                    "type": {
+                                        "type": "enum",
+                                        "name": "Bar",
+                                        "symbols": [
+                                            "bar0",
+                                            "bar1",
+                                            "bar2"
+                                        ],
+                                        "default": "bar0"
+                                    }
                                 }
                             ]
                         }
@@ -235,14 +237,16 @@ fn 
avro_3787_deserialize_union_with_unknown_symbol_no_ref() -> TestResult {
                             "name": "BarParent",
                             "fields": [
                                 {
-                                    "type": "enum",
                                     "name": "Bar",
-                                    "symbols":
-                                    [
-                                        "bar0",
-                                        "bar1"
-                                    ],
-                                    "default": "bar0"
+                                    "type": {
+                                        "name": "Bar",
+                                        "type": "enum",
+                                        "symbols": [
+                                            "bar0",
+                                            "bar1"
+                                        ],
+                                        "default": "bar0"
+                                    }
                                 }
                             ]
                         }
diff --git a/avro/tests/schema.rs b/avro/tests/schema.rs
index 3bcc895..89bfd68 100644
--- a/avro/tests/schema.rs
+++ b/avro/tests/schema.rs
@@ -1850,8 +1850,10 @@ fn 
test_avro_3851_read_default_value_for_array_record_field() -> TestResult {
                 "type": "int"
             },  {
                 "name": "f2",
-                "type": "array",
-                "items": "int",
+                "type": {
+                    "type": "array",
+                    "items": "int"
+                },
                 "default": [1, 2, 3]
             }
         ]
@@ -1892,8 +1894,10 @@ fn 
test_avro_3851_read_default_value_for_map_record_field() -> TestResult {
                 "type": "int"
             },  {
                 "name": "f2",
-                "type": "map",
-                "values": "string",
+                "type": {
+                    "type": "map",
+                    "values": "string"
+                },
                 "default": { "a": "A", "b": "B", "c": "C" }
             }
         ]
diff --git a/avro/tests/validators.rs b/avro/tests/validators.rs
index ab37248..55659be 100644
--- a/avro/tests/validators.rs
+++ b/avro/tests/validators.rs
@@ -72,9 +72,12 @@ fn avro_3900_custom_validator_with_spec_invalid_names() -> 
TestResult {
                 "type": "int"
             },
             {
-                "type": "enum",
-                "name": "Test",
-                "symbols": ["A-B", "B-A"]
+                "name": "field-name",
+                "type": {
+                    "type": "enum",
+                    "name": "Test",
+                    "symbols": ["A-B", "B-A"]
+                }
             }
         ]
     }"#;
diff --git a/avro_derive/src/attributes/avro.rs 
b/avro_derive/src/attributes/avro.rs
index ea171b5..1116267 100644
--- a/avro_derive/src/attributes/avro.rs
+++ b/avro_derive/src/attributes/avro.rs
@@ -60,15 +60,15 @@ impl ContainerAttributes {
         if self.name.is_some() {
             super::warn(
                 span,
-                "`#[avro(name = \"...\")]` is deprecated.",
-                "Use `#[serde(rename = \"...\")]` instead.",
+                r#"`#[avro(name = "...")]` is deprecated."#,
+                r#"Use `#[serde(rename = "...")]` instead."#,
             )
         }
         if self.rename_all != RenameRule::None {
             super::warn(
                 span,
-                "`#[avro(rename_all = \"..\")]` is deprecated",
-                "Use `#[serde(rename_all = \"..\")]` instead",
+                r#"`#[avro(rename_all = "..")]` is deprecated"#,
+                r#"Use `#[serde(rename_all = "..")]` instead"#,
             )
         }
     }
@@ -92,8 +92,8 @@ impl VariantAttributes {
         if self.rename.is_some() {
             super::warn(
                 span,
-                "`#[avro(rename = \"..\")]` is deprecated",
-                "Use `#[serde(rename = \"..\")]` instead",
+                r#"`#[avro(rename = "..")]` is deprecated"#,
+                r#"Use `#[serde(rename = "..")]` instead"#,
             )
         }
     }
@@ -171,15 +171,15 @@ impl FieldAttributes {
         if !self.alias.is_empty() {
             super::warn(
                 span,
-                "`#[avro(alias = \"..\")]` is deprecated",
-                "Use `#[serde(alias = \"..\")]` instead",
+                r#"`#[avro(alias = "..")]` is deprecated"#,
+                r#"Use `#[serde(alias = "..")]` instead"#,
             )
         }
         if self.rename.is_some() {
             super::warn(
                 span,
-                "`#[avro(rename = \"..\")]` is deprecated",
-                "Use `#[serde(rename = \"..\")]` instead",
+                r#"`#[avro(rename = "..")]` is deprecated"#,
+                r#"Use `#[serde(rename = "..")]` instead"#,
             )
         }
         if self.skip {
diff --git a/avro_derive/src/attributes/mod.rs 
b/avro_derive/src/attributes/mod.rs
index cc259f1..12f8b20 100644
--- a/avro_derive/src/attributes/mod.rs
+++ b/avro_derive/src/attributes/mod.rs
@@ -70,7 +70,7 @@ impl NamedTypeOptions {
         if serde.rename_all.deserialize != serde.rename_all.serialize {
             errors.push(syn::Error::new(
                 span,
-                "AvroSchema derive does not support different rename rules for 
serializing and deserializing (`rename_all(serialize = \"..\", deserialize = 
\"..\")`)"
+                r#"AvroSchema derive does not support different rename rules 
for serializing and deserializing (`rename_all(serialize = "..", deserialize = 
"..")`)"#
             ));
         }
 
@@ -78,13 +78,13 @@ impl NamedTypeOptions {
         if avro.name.is_some() && avro.name != serde.rename {
             errors.push(syn::Error::new(
                 span,
-                "#[avro(name = \"..\")] must match #[serde(rename = \"..\")], 
it's also deprecated. Please use only `#[serde(rename = \"..\")]`",
+                r#"#[avro(name = "..")] must match #[serde(rename = "..")], 
it's also deprecated. Please use only `#[serde(rename = "..")]`"#,
             ));
         }
         if avro.rename_all != RenameRule::None && serde.rename_all.serialize 
!= avro.rename_all {
             errors.push(syn::Error::new(
                 span,
-                "#[avro(rename_all = \"..\")] must match #[serde(rename_all = 
\"..\")], it's also deprecated. Please use only `#[serde(rename_all = 
\"..\")]`",
+                r#"#[avro(rename_all = "..")] must match #[serde(rename_all = 
"..")], it's also deprecated. Please use only `#[serde(rename_all = "..")]`"#,
             ));
         }
         if serde.transparent
@@ -154,7 +154,7 @@ impl VariantOptions {
         if avro.rename.is_some() && serde.rename != avro.rename {
             errors.push(syn::Error::new(
                 span,
-                "`#[avro(rename = \"..\")]` must match `#[serde(rename = 
\"..\")]`, it's also deprecated. Please use only `#[serde(rename = \"..\")]`"
+                r#"`#[avro(rename = "..")]` must match `#[serde(rename = 
"..")]`, it's also deprecated. Please use only `#[serde(rename = "..")]`"#
             ));
         }
 
@@ -194,7 +194,7 @@ impl With {
                         syn::Error::new(
                             span,
                             format!(
-                                "AvroSchema: Expected a path for `#[serde(with 
= \"..\")]`: {err:?}"
+                                r#"AvroSchema: Expected a path for 
`#[serde(with = "..")]`: {err:?}"#
                             ),
                         )
                     })?;
@@ -202,7 +202,7 @@ impl With {
                 } else {
                     Err(syn::Error::new(
                         span,
-                        "`#[avro(with)]` requires `#[serde(with = 
\"some_module\")]` or provide a function to call `#[avro(with = some_fn)]`",
+                        r#"`#[avro(with)]` requires `#[serde(with = 
"some_module")]` or provide a function to call `#[avro(with = some_fn)]`"#,
                     ))
                 }
             }
@@ -263,13 +263,13 @@ impl FieldOptions {
         if avro.rename.is_some() && serde.rename != avro.rename {
             errors.push(syn::Error::new(
                 span,
-                "`#[avro(rename = \"..\")]` must match `#[serde(rename = 
\"..\")]`, it's also deprecated. Please use only `#[serde(rename = \"..\")]`"
+                r#"`#[avro(rename = "..")]` must match `#[serde(rename = 
"..")]`, it's also deprecated. Please use only `#[serde(rename = "..")]`"#
             ));
         }
         if !avro.alias.is_empty() && serde.alias != avro.alias {
             errors.push(syn::Error::new(
                 span,
-                "`#[avro(alias = \"..\")]` must match `#[serde(alias = 
\"..\")]`, it's also deprecated. Please use only `#[serde(alias = \"..\")]`"
+                r#"`#[avro(alias = "..")]` must match `#[serde(alias = 
"..")]`, it's also deprecated. Please use only `#[serde(alias = "..")]`"#
             ));
         }
         if ((serde.skip_serializing && !serde.skip_deserializing)
@@ -278,7 +278,7 @@ impl FieldOptions {
         {
             errors.push(syn::Error::new(
                 span,
-                "`#[serde(skip_serializing)]` and 
`#[serde(skip_serializing_if)]` require `#[avro(default = \"..\")]`"
+                r#"`#[serde(skip_serializing)]` and 
`#[serde(skip_serializing_if)]` require `#[avro(default = "..")]`"#
             ));
         }
         let with = match With::from_avro_and_serde(&avro.with, &serde.with, 
span) {
diff --git a/avro_derive/tests/derive.rs b/avro_derive/tests/derive.rs
index 9c26152..c9f5d44 100644
--- a/avro_derive/tests/derive.rs
+++ b/avro_derive/tests/derive.rs
@@ -2088,13 +2088,17 @@ fn avro_rs_401_supported_type_variants() {
             },
             {
                 "name":"six",
-                "type":"array",
-                "items":"int"
+                "type":{
+                    "type":"array",
+                    "items":"int"
+                }
             },
             {
                 "name":"seven",
-                "type":"array",
-                "items":"int"
+                "type": {
+                    "type":"array",
+                    "items":"int"
+                }
             }
         ]
     }
diff --git a/avro_test_helper/src/data.rs b/avro_test_helper/src/data.rs
index 8f61777..3713e83 100644
--- a/avro_test_helper/src/data.rs
+++ b/avro_test_helper/src/data.rs
@@ -362,8 +362,8 @@ pub const OTHER_ATTRIBUTES_EXAMPLES: &[(&str, bool)] = &[
                 "cp_int": 1,
                 "cp_array": [ 1, 2, 3, 4],
                 "fields": [
-                    {"name": "f1", "type": "fixed", "size": 16, "cp_object": 
{"a":1,"b":2}},
-                    {"name": "f2", "type": "fixed", "size": 8, "cp_null": null}
+                    {"name": "f1", "type": {"name": "f1", "type": "fixed", 
"size": 16}, "cp_object": {"a":1,"b":2}},
+                    {"name": "f2", "type": {"name": "f2", "type": "fixed", 
"size": 8}, "cp_null": null}
                 ]
             }"#,
         true,


Reply via email to