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,