martin-g commented on code in PR #458:
URL: https://github.com/apache/avro-rs/pull/458#discussion_r2788189355


##########
avro/src/serde/de.rs:
##########
@@ -799,9 +850,19 @@ impl<'de> de::Deserializer<'de> for Deserializer<'de> {
             Value::Record(fields) => 
visitor.visit_enum(EnumDeserializer::new(fields)),
             Value::String(field) => 
visitor.visit_enum(EnumUnitDeserializer::new(field)),
             Value::Union(idx, inner) => {
-                if (*idx as usize) < variants.len() {
+                // if we came here from a some, we need to check if we are 
deserializing a
+                // non-newtype enum
+                if self.deserializing_some
+                    && let Value::Enum(_index, field) = inner.deref()
+                    && variants.contains(&&**field)
+                {
+                    return 
visitor.visit_enum(EnumUnitDeserializer::new(field));
+                }
+                // Assume `null` is the first branch if deserializing some so 
decrement the variant index

Review Comment:
   This assumption is not guaranteed by the spec :-/
   We should add some index/length checks to avoid panics/overflows.



##########
avro/src/serde/de.rs:
##########
@@ -638,6 +664,21 @@ impl<'de> de::Deserializer<'de> for Deserializer<'de> {
                 let d_bytes: [u8; 12] = d.into();
                 visitor.visit_byte_buf(Vec::from(d_bytes))
             }
+            Value::Union(i, x) => {
+                if matches!(x.deref(), Value::Union(_, _)) {
+                    Err(de::Error::custom(format!(
+                        "Directly nested union types are not supported. Got 
Value::Union({i}, {x:?})"
+                    )))
+                } else {
+                    Self::new(x.deref())
+                        .deserialize_byte_buf(visitor)
+                        .map_err(|e| {
+                            de::Error::custom(format!(
+                                "Attempted to deserialize Value::Union({i}, 
{x:?}) as bytes: {e:?}"

Review Comment:
   ```suggestion
                                   "Attempted to deserialize Value::Union({i}, 
{x:?}) as byte_buf: {e:?}"
   ```



##########
avro/src/serde/ser_schema.rs:
##########
@@ -1460,19 +1477,37 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
                 encode_int(variant_index as i32, &mut self.writer)
             }
             Schema::Union(union_schema) => {
-                if variant_index as usize >= union_schema.schemas.len() {
+                // If we came here from a some, we need to check if we are 
serializing a
+                // non-newtype enum
+                if self.serializing_some {
+                    for (i, variant_schema) in 
union_schema.schemas.iter().enumerate() {
+                        match variant_schema {
+                            Schema::Enum(enum_schema) if enum_schema.name.name 
== name => {
+                                if variant_index as usize >= 
enum_schema.symbols.len() {
+                                    return Err(create_error(format!(
+                                        "Variant index out of bounds: {}. The 
Enum schema has '{}' symbols",
+                                        variant_index,
+                                        enum_schema.symbols.len()
+                                    )));
+                                }
+                                encode_int(i as i32, &mut self.writer)?;
+                                return encode_int(variant_index as i32, &mut 
self.writer);
+                            }
+                            _ => { /* skip */ }
+                        }
+                    }
+                }
+                let branch_index = variant_index as usize + 
usize::from(self.serializing_some);

Review Comment:
   Again, this assumes that the `null` is the first variant in the union.



##########
avro/tests/nullable_union.rs:
##########
@@ -0,0 +1,982 @@
+use apache_avro::{AvroResult, Reader, Schema, Writer, from_value, 
types::Value};
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+
+struct TestCase<'a, T> {
+    input: T,
+    schema: &'a Schema,
+    expected_avro: &'a Value,
+}
+
+fn test_serialize<T>(test_case: TestCase<T>) -> AvroResult<()>
+where
+    T: Serialize + std::fmt::Debug,
+{
+    let mut writer = Writer::new(test_case.schema, Vec::new())?;
+    writer.append_ser(&test_case.input)?;
+    let bytes = writer.into_inner()?;
+    let mut reader = Reader::with_schema(test_case.schema, &bytes[..])?;
+    let read_avro_value = reader.next().unwrap()?;
+    assert_eq!(
+        &read_avro_value, test_case.expected_avro,
+        "serialization is not correct: expected: {:?}, got: {:?}, input: {:?}",
+        test_case.expected_avro, read_avro_value, test_case.input
+    );
+    Ok(())
+}
+
+fn test_deserialize<T>(test_case: TestCase<T>) -> AvroResult<()>
+where
+    T: DeserializeOwned + std::fmt::Debug + std::cmp::PartialEq,
+{
+    let deserialized: T = from_value(test_case.expected_avro)?;
+    assert_eq!(
+        deserialized, test_case.input,
+        "deserialization is not correct: expected: {:?}, got: {:?}",
+        test_case.input, deserialized,
+    );
+    Ok(())
+}
+
+mod nullable_enum {
+    use super::*;
+
+    const NULLABLE_ENUM_SCHEMA: &str = r#"
+    {
+        "name": "MyUnion",
+        "type": [
+            "null",
+            {
+                "type": "enum",
+                "name": "MyEnum",
+                "symbols": ["A", "B"]
+            }
+        ]
+    }
+    "#;
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyEnum {
+        A,
+        B,
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionNullable {
+        Null,
+        MyEnum(MyEnum),
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionAvroJsonEncoding {
+        MyEnum(MyEnum),
+    }
+
+    fn schema() -> Schema {
+        Schema::parse_str(NULLABLE_ENUM_SCHEMA).unwrap()
+    }
+
+    fn null_variant_expected_avro() -> Value {
+        Value::Union(0, Box::new(Value::Null))
+    }
+
+    fn a_variant_expected_avro() -> Value {
+        Value::Union(1, Box::new(Value::Enum(0, "A".to_string())))
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<MyEnum>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<MyEnum>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::MyEnum(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::MyEnum(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_my_enum_a() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_my_enum_a() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_avro_json_encoding_compatible_my_enum_a() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_avro_json_encoding_compatible_my_enum_a() -> AvroResult<()> 
{
+        test_deserialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+}
+
+mod nullable_primitive_int {
+    use super::*;
+
+    const NULLABLE_INT_SCHEMA: &str = r#"
+    {
+        "name": "MyUnion",
+        "type": [
+            "null",
+            "int"

Review Comment:
   Please add a test where the `null` is not the first variant too.



##########
avro/tests/nullable_union.rs:
##########
@@ -0,0 +1,982 @@
+use apache_avro::{AvroResult, Reader, Schema, Writer, from_value, 
types::Value};
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+
+struct TestCase<'a, T> {
+    input: T,
+    schema: &'a Schema,
+    expected_avro: &'a Value,
+}
+
+fn test_serialize<T>(test_case: TestCase<T>) -> AvroResult<()>
+where
+    T: Serialize + std::fmt::Debug,
+{
+    let mut writer = Writer::new(test_case.schema, Vec::new())?;
+    writer.append_ser(&test_case.input)?;
+    let bytes = writer.into_inner()?;
+    let mut reader = Reader::with_schema(test_case.schema, &bytes[..])?;
+    let read_avro_value = reader.next().unwrap()?;
+    assert_eq!(
+        &read_avro_value, test_case.expected_avro,
+        "serialization is not correct: expected: {:?}, got: {:?}, input: {:?}",
+        test_case.expected_avro, read_avro_value, test_case.input
+    );
+    Ok(())
+}
+
+fn test_deserialize<T>(test_case: TestCase<T>) -> AvroResult<()>
+where
+    T: DeserializeOwned + std::fmt::Debug + std::cmp::PartialEq,
+{
+    let deserialized: T = from_value(test_case.expected_avro)?;
+    assert_eq!(
+        deserialized, test_case.input,
+        "deserialization is not correct: expected: {:?}, got: {:?}",
+        test_case.input, deserialized,
+    );
+    Ok(())
+}
+
+mod nullable_enum {
+    use super::*;
+
+    const NULLABLE_ENUM_SCHEMA: &str = r#"
+    {
+        "name": "MyUnion",
+        "type": [
+            "null",
+            {
+                "type": "enum",
+                "name": "MyEnum",
+                "symbols": ["A", "B"]
+            }
+        ]
+    }
+    "#;
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyEnum {
+        A,
+        B,
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionNullable {
+        Null,
+        MyEnum(MyEnum),
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionAvroJsonEncoding {
+        MyEnum(MyEnum),
+    }
+
+    fn schema() -> Schema {
+        Schema::parse_str(NULLABLE_ENUM_SCHEMA).unwrap()
+    }
+
+    fn null_variant_expected_avro() -> Value {
+        Value::Union(0, Box::new(Value::Null))
+    }
+
+    fn a_variant_expected_avro() -> Value {
+        Value::Union(1, Box::new(Value::Enum(0, "A".to_string())))
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<MyEnum>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<MyEnum>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::MyEnum(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::MyEnum(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_my_enum_a() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_my_enum_a() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_avro_json_encoding_compatible_my_enum_a() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_avro_json_encoding_compatible_my_enum_a() -> AvroResult<()> 
{
+        test_deserialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+}
+
+mod nullable_primitive_int {
+    use super::*;
+
+    const NULLABLE_INT_SCHEMA: &str = r#"
+    {
+        "name": "MyUnion",
+        "type": [
+            "null",
+            "int"
+        ]
+    }
+    "#;
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionNullable {
+        Null,
+        Int(i32),
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionAvroJsonEncoding {
+        #[serde(rename = "int")]
+        Int(i32),
+    }
+
+    fn schema() -> Schema {
+        Schema::parse_str(NULLABLE_INT_SCHEMA).unwrap()
+    }
+
+    fn null_variant_expected_avro() -> Value {
+        Value::Union(0, Box::new(Value::Null))
+    }
+
+    fn int_variant_expected_avro(v: i32) -> Value {
+        Value::Union(1, Box::new(Value::Int(v)))
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<i32>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<i32>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_int_42() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::Int(42),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_int_42() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::Int(42),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_int_42() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(42_i32),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_int_42() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(42_i32),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn serialize_avro_json_encoding_compatible_int_42() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::Int(42)),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn deserialize_avro_json_encoding_compatible_int_42() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::Int(42)),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+}
+
+mod nullable_record {
+    use super::*;
+
+    const NULLABLE_RECORD_SCHEMA: &str = r#"
+    {
+        "name": "MyUnion",
+        "type": [
+            "null",
+            {
+                "type": "record",
+                "name": "MyRecord",
+                "fields": [
+                    {"name": "a", "type": "int"}
+                ]
+            }
+        ]
+    }
+    "#;
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct MyRecord {
+        a: i32,
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionNullable {
+        Null,
+        MyRecord(MyRecord),
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionAvroJsonEncoding {
+        MyRecord(MyRecord),
+    }
+
+    fn schema() -> Schema {
+        Schema::parse_str(NULLABLE_RECORD_SCHEMA).unwrap()
+    }
+
+    fn null_variant_expected_avro() -> Value {
+        Value::Union(0, Box::new(Value::Null))
+    }
+
+    fn record_variant_expected_avro(a: i32) -> Value {
+        Value::Union(
+            1,
+            Box::new(Value::Record(vec![("a".to_string(), Value::Int(a))])),
+        )
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<MyRecord>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<MyRecord>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::MyRecord(MyRecord { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::MyRecord(MyRecord { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_my_record_a_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyRecord { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_my_record_a_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyRecord { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn serialize_avro_json_encoding_compatible_my_record_a_27() -> 
AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyRecord(MyRecord { a: 27 })),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_avro_json_encoding_compatible_my_record_a_27() -> 
AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyRecord(MyRecord { a: 27 })),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+}
+
+mod nullable_int_enum_record {
+    use super::*;
+
+    const NULLABLE_INT_ENUM_RECORD_SCHEMA: &str = r#"
+    {
+        "name": "MyUnion",
+        "type": [
+            "null",
+            "int",
+            {
+                "type": "enum",
+                "name": "MyEnum",
+                "symbols": ["A", "B"]
+            },
+            {
+                "type": "record",
+                "name": "MyRecord",
+                "fields": [
+                    {"name": "a", "type": "int"}
+                ]
+            }
+        ]
+    }
+    "#;
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyEnum {
+        A,
+        B,
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct MyRecord {
+        a: i32,
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionNullable {
+        Null,
+        Int(i32),
+        MyEnum(MyEnum),
+        MyRecord(MyRecord),
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    #[serde(untagged)]
+    enum MyUnionUntagged {
+        Int(i32),
+        MyEnum(MyEnum),
+        MyRecord(MyRecord),
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionAvroJsonEncoding {
+        Int(i32),
+        MyEnum(MyEnum),
+        MyRecord(MyRecord),
+    }
+
+    fn schema() -> Schema {
+        Schema::parse_str(NULLABLE_INT_ENUM_RECORD_SCHEMA).unwrap()
+    }
+
+    fn null_variant_expected_avro() -> Value {
+        Value::Union(0, Box::new(Value::Null))
+    }
+
+    fn int_variant_expected_avro(v: i32) -> Value {
+        Value::Union(1, Box::new(Value::Int(v)))
+    }
+
+    fn a_variant_expected_avro() -> Value {
+        Value::Union(2, Box::new(Value::Enum(0, "A".to_string())))
+    }
+
+    fn record_variant_expected_avro(a: i32) -> Value {
+        Value::Union(
+            3,
+            Box::new(Value::Record(vec![("a".to_string(), Value::Int(a))])),
+        )
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::Null,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<MyUnionAvroJsonEncoding>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_untagged_null() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: None::<MyUnionUntagged>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_untagged_null() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: None::<MyUnionUntagged>,
+            schema: &schema(),
+            expected_avro: &null_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_int_42() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::Int(42),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_int_42() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::Int(42),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_int_42() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::Int(42)),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_int_42() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::Int(42)),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_untagged_int_42() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionUntagged::Int(42)),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_untagged_int_42() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionUntagged::Int(42)),
+            schema: &schema(),
+            expected_avro: &int_variant_expected_avro(42),
+        })
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::MyEnum(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::MyEnum(MyEnum::A),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_my_enum_a() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_my_enum_a() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_untagged_my_enum_a() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionUntagged::MyEnum(MyEnum::A)),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_untagged_my_enum_a() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionUntagged::MyEnum(MyEnum::A)),
+            schema: &schema(),
+            expected_avro: &a_variant_expected_avro(),
+        })
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::MyRecord(MyRecord { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::MyRecord(MyRecord { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_my_record_a_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyRecord(MyRecord { a: 27 })),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_my_record_a_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyRecord(MyRecord { a: 27 })),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_untagged_my_record_a_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionUntagged::MyRecord(MyRecord { a: 27 })),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_untagged_my_record_a_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionUntagged::MyRecord(MyRecord { a: 27 })),
+            schema: &schema(),
+            expected_avro: &record_variant_expected_avro(27),
+        })
+    }
+}
+
+mod nullable_untagged_pitfall {
+    use super::*;
+
+    const NULLABLE_RECORD_SCHEMA: &str = r#"
+    {
+        "name": "MyUnion",
+        "type": [
+            "null",
+            {
+                "type": "record",
+                "name": "MyRecordA",
+                "fields": [
+                    {"name": "a", "type": "int"}
+                ]
+            },
+            {
+                "type": "record",
+                "name": "MyRecordB",
+                "fields": [
+                    {"name": "a", "type": "int"}
+                ]
+            }
+        ]
+    }
+    "#;
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct MyRecordA {
+        a: i32,
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    struct MyRecordB {
+        a: i32,
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionNullable {
+        Null,
+        MyRecordA(MyRecordA),
+        MyRecordB(MyRecordB),
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    #[serde(untagged)]
+    enum MyUnionUntagged {
+        MyRecordA(MyRecordA),
+        MyRecordB(MyRecordB),
+    }
+
+    #[derive(Debug, Serialize, Deserialize, PartialEq)]
+    enum MyUnionAvroJsonEncoding {
+        MyRecordA(MyRecordA),
+        MyRecordB(MyRecordB),
+    }
+
+    fn schema() -> Schema {
+        Schema::parse_str(NULLABLE_RECORD_SCHEMA).unwrap()
+    }
+
+    fn record_a_variant_expected_avro(a: i32) -> Value {
+        Value::Union(
+            1,
+            Box::new(Value::Record(vec![("a".to_string(), Value::Int(a))])),
+        )
+    }
+
+    fn record_b_variant_expected_avro(a: i32) -> Value {
+        Value::Union(
+            2,
+            Box::new(Value::Record(vec![("a".to_string(), Value::Int(a))])),
+        )
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::MyRecordA(MyRecordA { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_a_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::MyRecordA(MyRecordA { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_a_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_my_record_a_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyRecordA(MyRecordA { a: 27 
})),
+            schema: &schema(),
+            expected_avro: &record_a_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_my_record_a_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyRecordA(MyRecordA { a: 27 
})),
+            schema: &schema(),
+            expected_avro: &record_a_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_untagged_my_record_a_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionUntagged::MyRecordA(MyRecordA { a: 27 })),
+            schema: &schema(),
+            expected_avro: &record_a_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_untagged_my_record_a_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionUntagged::MyRecordA(MyRecordA { a: 27 })),
+            schema: &schema(),
+            expected_avro: &record_a_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn serialize_null_variant_enum_my_record_b_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: MyUnionNullable::MyRecordB(MyRecordB { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_b_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_null_variant_enum_my_record_b_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: MyUnionNullable::MyRecordB(MyRecordB { a: 27 }),
+            schema: &schema(),
+            expected_avro: &record_b_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_my_record_b_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyRecordB(MyRecordB { a: 27 
})),
+            schema: &schema(),
+            expected_avro: &record_b_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn deserialize_rusty_my_record_b_27() -> AvroResult<()> {
+        test_deserialize(TestCase {
+            input: Some(MyUnionAvroJsonEncoding::MyRecordB(MyRecordB { a: 27 
})),
+            schema: &schema(),
+            expected_avro: &record_b_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    fn serialize_rusty_untagged_my_record_b_27() -> AvroResult<()> {
+        test_serialize(TestCase {
+            input: Some(MyUnionUntagged::MyRecordB(MyRecordB { a: 27 })),
+            schema: &schema(),
+            expected_avro: &record_b_variant_expected_avro(27),
+        })
+    }
+
+    #[test]
+    #[ignore]

Review Comment:
   Please add a comment why it is ignored



##########
avro/src/serde/ser_schema.rs:
##########
@@ -1835,17 +1878,17 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
 
         match schema {
             Schema::Union(union_schema) => {
-                let variant_schema = union_schema
-                    .schemas
-                    .get(variant_index as usize)
-                    .ok_or_else(|| {
-                        create_error(format!(
-                            "Cannot find variant at position {variant_index} 
in {union_schema:?}"
-                        ))
-                    })?;
+                let branch_index = variant_index as usize + 
usize::from(self.serializing_some);
+                if branch_index >= union_schema.schemas.len() {
+                    return Err(create_error(format!(
+                        "Variant index out of bounds: {}. The union schema has 
'{}' schemas",
+                        variant_index,

Review Comment:
   ```suggestion
                           branch_index,
   ```



##########
avro/src/serde/ser_schema.rs:
##########
@@ -629,6 +630,22 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
             root_schema: schema,
             names,
             enclosing_namespace,
+            serializing_some: false,
+        }
+    }
+
+    pub fn new_serializing_some(

Review Comment:
   ```suggestion
       fn new_serializing_some(
   ```



##########
avro/src/serde/ser_schema.rs:
##########
@@ -1703,17 +1742,21 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
 
         match schema {
             Schema::Union(union_schema) => {
-                let variant_schema = union_schema
-                    .schemas
-                    .get(variant_index as usize)
-                    .ok_or_else(|| {
-                        create_error(format!(
-                            "Cannot find a variant at position {variant_index} 
in {union_schema:?}"
-                        ))
-                    })?;
+                let branch_index = variant_index as usize + 
usize::from(self.serializing_some);
+                if branch_index >= union_schema.schemas.len() {
+                    return Err(create_error(format!(
+                        "Variant index out of bounds: {}. The union schema has 
'{}' schemas",
+                        variant_index,

Review Comment:
   ```suggestion
                           branch_index,
   ```



##########
avro/src/serde/ser_schema.rs:
##########
@@ -1524,17 +1559,21 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
 
         match schema {
             Schema::Union(union_schema) => {
-                let variant_schema = union_schema
-                    .schemas
-                    .get(variant_index as usize)
-                    .ok_or_else(|| {
-                        create_error(format!(
-                            "No variant schema at position {variant_index} for 
{union_schema:?}"
-                        ))
-                    })?;
+                let branch_index = variant_index as usize + 
usize::from(self.serializing_some);
+                if branch_index >= union_schema.schemas.len() {
+                    return Err(create_error(format!(
+                        "Variant index out of bounds: {}. The union schema has 
'{}' schemas",
+                        variant_index,

Review Comment:
   ```suggestion
                           branch_index,
   ```



##########
avro/src/serde/de.rs:
##########
@@ -75,7 +76,17 @@ struct UnionDeserializer<'de> {
 
 impl<'de> Deserializer<'de> {
     pub fn new(input: &'de Value) -> Self {
-        Deserializer { input }
+        Deserializer {
+            input,
+            deserializing_some: false,
+        }
+    }
+
+    pub fn new_deserializing_some(input: &'de Value) -> Self {

Review Comment:
   ```suggestion
       fn new_deserializing_some(input: &'de Value) -> Self {
   ```



##########
avro/src/serde/ser_schema.rs:
##########
@@ -1460,19 +1477,37 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
                 encode_int(variant_index as i32, &mut self.writer)
             }
             Schema::Union(union_schema) => {
-                if variant_index as usize >= union_schema.schemas.len() {
+                // If we came here from a some, we need to check if we are 
serializing a
+                // non-newtype enum
+                if self.serializing_some {
+                    for (i, variant_schema) in 
union_schema.schemas.iter().enumerate() {
+                        match variant_schema {
+                            Schema::Enum(enum_schema) if enum_schema.name.name 
== name => {
+                                if variant_index as usize >= 
enum_schema.symbols.len() {
+                                    return Err(create_error(format!(
+                                        "Variant index out of bounds: {}. The 
Enum schema has '{}' symbols",
+                                        variant_index,
+                                        enum_schema.symbols.len()
+                                    )));
+                                }
+                                encode_int(i as i32, &mut self.writer)?;
+                                return encode_int(variant_index as i32, &mut 
self.writer);
+                            }

Review Comment:
   This should probably support Schema::Ref too, to refer to Schema::Enum



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to