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

Kriskras99 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 08cac64  fix: accept JSON string defaults for decimal fields in union 
(#580)
08cac64 is described below

commit 08cac64f87ced7b0571620da60c6e5c052625871
Author: Vyacheslav Mayorov <[email protected]>
AuthorDate: Tue Jul 7 23:19:03 2026 +0300

    fix: accept JSON string defaults for decimal fields in union (#580)
    
    * fix: accept JSON string defaults for decimal fields in union
    
    Per Avro 1.12.0 Specification, §"Complex Types / Records", the JSON
    encoding of a `bytes` field's default value is a string whose codepoints
    0-255 map to byte values 0-255 (e.g. `"\u00FF"`). The same section
    specifies that a union-typed field's default must correspond to the
    first schema that matches in the union.
    
    `decimal` is defined as a logical type over `bytes`, so this rule
    transitively applies: a nullable decimal field expressed as
    `[{bytes, logicalType: decimal}, null]` with a JSON string default
    requires `resolve_decimal` to accept `Value::String` when validating
    defaults at schema parse time. Before this change the parser rejected
    such schemas with `GetDefaultUnion(Decimal, String)`, even though
    Java and Python Avro accept them.
    
    The added arm walks the string's codepoints, rejecting any above
    0xFF, and returns a `Value::Decimal`. The precision check is skipped
    because the spec does not require a default's byte length to cover
    the declared precision — it only requires a valid `bytes` value.
    Wire-level decoded records always reach `resolve_decimal` as
    `Value::Bytes`, so this arm is exclusively a default-validation path.
    
    Tests cover `\u0000`, a full 0..=255 round-trip, codepoints > 0xFF
    being rejected, and end-to-end parsing of a nullable decimal record
    schema.
    
    * fix: review comments
    
    * chore: Fix test output
    
    ---------
    
    Co-authored-by: Laurent Valdes <[email protected]>
    Co-authored-by: Vyacheslav Mayorov <[email protected]>
    Co-authored-by: Kriskras99 <[email protected]>
---
 avro/src/error.rs |  2 +-
 avro/src/types.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 85 insertions(+), 2 deletions(-)

diff --git a/avro/src/error.rs b/avro/src/error.rs
index 425bf4f..bd9cbcb 100644
--- a/avro/src/error.rs
+++ b/avro/src/error.rs
@@ -188,7 +188,7 @@ pub enum Details {
     #[error("Expected Value::Duration or Value::Fixed(12), got: {0:?}")]
     ResolveDuration(Value),
 
-    #[error("Expected Value::Decimal, Value::Bytes or Value::Fixed, got: 
{0:?}")]
+    #[error("Expected Value::Decimal, Value::Bytes, Value::Fixed or 
Value::String, got: {0:?}")]
     ResolveDecimal(Value),
 
     #[error("Missing field in record: {0:?}")]
diff --git a/avro/src/types.rs b/avro/src/types.rs
index ad0258b..503f6cb 100644
--- a/avro/src/types.rs
+++ b/avro/src/types.rs
@@ -846,6 +846,26 @@ impl Value {
                     Ok(Value::Decimal(Decimal::from(bytes)))
                 }
             }
+
+            // Per spec §Records, a decimal value can be encoded as a JSON 
string
+            // whose codepoints (0-255) map directly to byte values. This 
applies
+            // to defaults and any other String → Decimal resolution. No 
precision
+            // check is performed — the bytes only need to be valid, not fit 
the
+            // declared precision.
+            Value::String(s) => {
+                let bytes = s
+                    .chars()
+                    .map(|c| {
+                        let cp = c as u32;
+                        if cp > 0xFF {
+                            
Err(Details::ResolveDecimal(Value::String(s.clone())).into())
+                        } else {
+                            Ok(cp as u8)
+                        }
+                    })
+                    .collect::<Result<Vec<u8>, Error>>()?;
+                Ok(Value::Decimal(Decimal::from(bytes)))
+            }
             other => Err(Details::ResolveDecimal(other).into()),
         }
     }
@@ -1776,6 +1796,69 @@ Field with name '"b"' is not a member of the map items"#,
         Ok(())
     }
 
+    #[test]
+    fn avro_rs_580_resolve_decimal_from_string_default() -> TestResult {
+        let value = Value::String("\u{0000}".to_string());
+        let resolved = value.resolve(&Schema::Decimal(DecimalSchema {
+            precision: 10,
+            scale: 4,
+            inner: InnerDecimalSchema::Bytes,
+        }))?;
+        assert_eq!(resolved, Value::Decimal(Decimal::from(vec![0u8])));
+
+        let mut all_bytes_str = String::new();
+        for b in 0u8..=255u8 {
+            all_bytes_str.push(char::from_u32(b as u32).unwrap());
+        }
+        let resolved = 
Value::String(all_bytes_str).resolve(&Schema::Decimal(DecimalSchema {
+            precision: 10,
+            scale: 0,
+            inner: InnerDecimalSchema::Bytes,
+        }))?;
+        assert_eq!(
+            resolved,
+            Value::Decimal(Decimal::from((0u8..=255u8).collect::<Vec<_>>()))
+        );
+
+        let value = Value::String("\u{0100}".to_string());
+        assert!(
+            value
+                .resolve(&Schema::Decimal(DecimalSchema {
+                    precision: 10,
+                    scale: 4,
+                    inner: InnerDecimalSchema::Bytes,
+                }))
+                .is_err()
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn avro_rs_580_parse_schema_with_nullable_decimal_string_default() -> 
TestResult {
+        let schema_json = r#"{
+            "type": "record",
+            "name": "NullableDecimal",
+            "fields": [
+                {
+                    "name": "amount",
+                    "type": [
+                        {
+                            "type": "bytes",
+                            "scale": 4,
+                            "precision": 10,
+                            "logicalType": "decimal"
+                        },
+                        "null"
+                    ],
+                    "default": "\u0000"
+                }
+            ]
+        }"#;
+        Schema::parse_str(schema_json)?;
+        Ok(())
+    }
+
     #[test]
     fn resolve_decimal_invalid_scale() {
         let value = Value::Decimal(Decimal::from(vec![1, 2]));
@@ -3326,7 +3409,7 @@ Field with name '"b"' is not a member of the map items"#,
             (
                 r#"{ "name": "NAME", "type": "bytes", "logicalType": 
"decimal", "precision": 4, "scale": 3 }"#,
                 Value::Float(123_f32),
-                "Expected Value::Decimal, Value::Bytes or Value::Fixed, got: 
Float(123.0)",
+                "Expected Value::Decimal, Value::Bytes, Value::Fixed or 
Value::String, got: Float(123.0)",
             ),
             (
                 r#"{ "name": "NAME", "type": "bytes" }"#,

Reply via email to