diff --git a/borsh/Cargo.toml b/borsh/Cargo.toml index e0a9465fe..b5ed5019d 100644 --- a/borsh/Cargo.toml +++ b/borsh/Cargo.toml @@ -36,6 +36,7 @@ borsh-derive = { path = "../borsh-derive", version = "~1.5.1", optional = true } hashbrown = { version = ">=0.11,<0.15.0", optional = true } bytes = { version = "1", optional = true } bson = { version = "2", optional = true } +serde_json = { version = "1", optional = true } [dev-dependencies] insta = "1.29.0" @@ -54,3 +55,5 @@ std = [] # Be sure that this is what you want before enabling this feature. rc = [] de_strict_order = [] +# Implements BorshSerialize and BorshDeserialize for serde_json::Value. +json = ["serde_json"] diff --git a/borsh/src/de/mod.rs b/borsh/src/de/mod.rs index bce4f1267..38438566d 100644 --- a/borsh/src/de/mod.rs +++ b/borsh/src/de/mod.rs @@ -453,6 +453,91 @@ impl BorshDeserialize for bson::oid::ObjectId { } } +#[cfg(feature = "json")] +impl BorshDeserialize for serde_json::Value { + #[inline] + fn deserialize_reader(reader: &mut R) -> Result { + let flag: u8 = BorshDeserialize::deserialize_reader(reader)?; + match flag { + 0 => Ok(Self::Null), + 1 => { + let b: bool = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::Bool(b)) + } + 2 => { + let n: serde_json::Number = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::Number(n)) + } + 3 => { + let s: String = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::String(s)) + } + 4 => { + let a: Vec = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::Array(a)) + } + 5 => { + let o: serde_json::Map<_, _> = BorshDeserialize::deserialize_reader(reader)?; + Ok(Self::Object(o)) + } + _ => { + let msg = format!( + "Invalid JSON value representation: {}. The first byte must be 0-5", + flag + ); + + Err(Error::new(ErrorKind::InvalidData, msg)) + } + } + } +} + +#[cfg(feature = "json")] +impl BorshDeserialize for serde_json::Number { + #[inline] + fn deserialize_reader(reader: &mut R) -> Result { + let flag: u8 = BorshDeserialize::deserialize_reader(reader)?; + match flag { + 0 => { + let u: u64 = BorshDeserialize::deserialize_reader(reader)?; + Ok(u.into()) + } + 1 => { + let i: i64 = BorshDeserialize::deserialize_reader(reader)?; + Ok(i.into()) + } + 2 => { + let f: f64 = BorshDeserialize::deserialize_reader(reader)?; + // This returns None if the number is a NaN or +/-Infinity, + // which are not valid JSON numbers. + Self::from_f64(f).ok_or_else(|| { + let msg = format!("Invalid JSON number: {}", f); + + Error::new(ErrorKind::InvalidData, msg) + }) + } + _ => { + let msg = format!( + "Invalid JSON number representation: {}. The first byte must be 0-2", + flag + ); + + Err(Error::new(ErrorKind::InvalidData, msg)) + } + } + } +} + +#[cfg(feature = "json")] +impl BorshDeserialize for serde_json::Map { + #[inline] + fn deserialize_reader(reader: &mut R) -> Result { + // The implementation here is identical to that of BTreeMap. + let vec = >::deserialize_reader(reader)?; + Ok(vec.into_iter().collect()) + } +} + impl BorshDeserialize for Cow<'_, T> where T: ToOwned + ?Sized, diff --git a/borsh/src/ser/mod.rs b/borsh/src/ser/mod.rs index bb1980806..df18a167d 100644 --- a/borsh/src/ser/mod.rs +++ b/borsh/src/ser/mod.rs @@ -317,6 +317,84 @@ impl BorshSerialize for bson::oid::ObjectId { } } +#[cfg(feature = "json")] +impl BorshSerialize for serde_json::Value { + #[inline] + fn serialize(&self, writer: &mut W) -> Result<()> { + match self { + Self::Null => 0_u8.serialize(writer), + Self::Bool(b) => { + 1_u8.serialize(writer)?; + b.serialize(writer) + } + Self::Number(n) => { + 2_u8.serialize(writer)?; + n.serialize(writer) + } + Self::String(s) => { + 3_u8.serialize(writer)?; + s.serialize(writer) + } + Self::Array(a) => { + 4_u8.serialize(writer)?; + a.serialize(writer) + } + Self::Object(o) => { + 5_u8.serialize(writer)?; + o.serialize(writer) + } + } + } +} + +#[cfg(feature = "json")] +impl BorshSerialize for serde_json::Number { + #[inline] + fn serialize(&self, writer: &mut W) -> Result<()> { + // A JSON number can either be a non-negative integer (represented in + // serde_json by a u64), a negative integer (by an i64), or a non-integer + // (by an f64). + // We identify these cases with the following single-byte discriminants: + // 0 - u64 + // 1 - i64 + // 2 - f64 + if let Some(u) = self.as_u64() { + 0_u8.serialize(writer)?; + return u.serialize(writer); + } + + if let Some(i) = self.as_i64() { + 1_u8.serialize(writer)?; + return i.serialize(writer); + } + + if let Some(f) = self.as_f64() { + 2_u8.serialize(writer)?; + return f.serialize(writer); + } + + unreachable!("number is neither a u64, i64, nor f64"); + } +} + +#[cfg(feature = "json")] +impl BorshSerialize for serde_json::Map { + #[inline] + fn serialize(&self, writer: &mut W) -> Result<()> { + // The implementation here is identical to that of BTreeMap. + u32::try_from(self.len()) + .map_err(|_| ErrorKind::InvalidData)? + .serialize(writer)?; + + for (key, value) in self { + key.serialize(writer)?; + value.serialize(writer)?; + } + + Ok(()) + } +} + impl BorshSerialize for VecDeque where T: BorshSerialize, diff --git a/borsh/tests/roundtrip/test_json.rs b/borsh/tests/roundtrip/test_json.rs new file mode 100644 index 000000000..3f955d485 --- /dev/null +++ b/borsh/tests/roundtrip/test_json.rs @@ -0,0 +1,59 @@ +use serde_json::json; + +#[test] +fn test_json_value() { + let original = json!({ + "null": null, + "true": true, + "false": false, + "zero": 0, + "positive_integer": 12345, + "negative_integer": -88888, + "positive_float": 123.45, + "negative_float": -888.88, + "positive_max": 1.7976931348623157e+308, + "negative_max": -1.7976931348623157e+308, + "string": "Larry", + "array_of_nulls": [null, null, null], + "array_of_numbers": [0, -1, 1, 1.1, -1.1, 34798324], + "array_of_strings": ["Larry", "Jake", "Pumpkin"], + "array_of_arrays": [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ], + "array_of_objects": [ + { + "name": "Larry", + "age": 30 + }, + { + "name": "Jake", + "age": 7 + }, + { + "name": "Pumpkin", + "age": 8 + } + ], + "object": { + "name": "Larry", + "age": 30, + "pets": [ + { + "name": "Jake", + "age": 7 + }, + { + "name": "Pumpkin", + "age": 8 + } + ] + } + }); + + let serialized = borsh::to_vec(&original).unwrap(); + let deserialized: serde_json::Value = borsh::from_slice(&serialized).unwrap(); + + assert_eq!(original, deserialized); +} diff --git a/borsh/tests/tests.rs b/borsh/tests/tests.rs index daaac5ec2..8d65c551f 100644 --- a/borsh/tests/tests.rs +++ b/borsh/tests/tests.rs @@ -50,6 +50,8 @@ mod roundtrip { mod test_cells; #[cfg(feature = "rc")] mod test_rc; + #[cfg(feature = "json")] + mod test_json; #[cfg(feature = "derive")] mod requires_derive_category {