From ed9f9f819c7288ae73bd01cd848ae174c3cb165d Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Wed, 26 Nov 2025 15:28:48 -0500 Subject: [PATCH] feat: Support serde_json's arbitrary_precision feature --- Cargo.toml | 4 ++ src/ser.rs | 105 ++++++++++++++++++++++++++++-- tests/test_arbitrary_precision.rs | 57 ++++++++++++++++ 3 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 tests/test_arbitrary_precision.rs diff --git a/Cargo.toml b/Cargo.toml index 4a714a3..1ce336f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ documentation = "https://docs.rs/crate/pythonize/" [dependencies] serde = { version = "1.0", default-features = false, features = ["std"] } +serde_json = { version = "1.0", optional = true } pyo3 = { version = "0.27", default-features = false } [dev-dependencies] @@ -22,3 +23,6 @@ serde_json = "1.0" serde_bytes = "0.11" maplit = "1.0.2" serde_path_to_error = "0.1.15" + +[features] +arbitrary_precision = ["serde_json", "serde_json/arbitrary_precision"] diff --git a/src/ser.rs b/src/ser.rs index c8e6dd1..425b358 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -4,6 +4,8 @@ use pyo3::types::{ PyDict, PyDictMethods, PyList, PyListMethods, PyMapping, PySequence, PyString, PyTuple, PyTupleMethods, }; +#[cfg(feature = "arbitrary_precision")] +use pyo3::types::{PyAnyMethods, PyFloat, PyInt}; use pyo3::{Bound, BoundObject, IntoPyObject, PyAny, PyResult, Python}; use serde::{ser, Serialize}; @@ -229,6 +231,21 @@ pub struct PythonStructVariantSerializer<'py, P: PythonizeTypes> { inner: PythonStructDictSerializer<'py, P>, } +#[cfg(feature = "arbitrary_precision")] +#[doc(hidden)] +pub enum StructSerializer<'py, P: PythonizeTypes> { + Struct(PythonStructDictSerializer<'py, P>), + Number { + py: Python<'py>, + number_string: Option, + _types: PhantomData

, + }, +} + +#[cfg(not(feature = "arbitrary_precision"))] +#[doc(hidden)] +pub type StructSerializer<'py, P> = PythonStructDictSerializer<'py, P>; + #[doc(hidden)] pub struct PythonStructDictSerializer<'py, P: PythonizeTypes> { py: Python<'py>, @@ -266,7 +283,7 @@ impl<'py, P: PythonizeTypes> ser::Serializer for Pythonizer<'py, P> { type SerializeTupleStruct = PythonCollectionSerializer<'py, P>; type SerializeTupleVariant = PythonTupleVariantSerializer<'py, P>; type SerializeMap = PythonMapSerializer<'py, P>; - type SerializeStruct = PythonStructDictSerializer<'py, P>; + type SerializeStruct = StructSerializer<'py, P>; type SerializeStructVariant = PythonStructVariantSerializer<'py, P>; fn serialize_bool(self, v: bool) -> Result> { @@ -439,12 +456,34 @@ impl<'py, P: PythonizeTypes> ser::Serializer for Pythonizer<'py, P> { self, name: &'static str, len: usize, - ) -> Result> { - Ok(PythonStructDictSerializer { - py: self.py, - builder: P::NamedMap::builder(self.py, len, name)?, - _types: PhantomData, - }) + ) -> Result> { + #[cfg(feature = "arbitrary_precision")] + { + // With arbitrary_precision enabled, a serde_json::Number serializes as a "$serde_json::private::Number" + // struct with a "$serde_json::private::Number" field, whose value is the String in Number::n. + if name == "$serde_json::private::Number" && len == 1 { + return Ok(StructSerializer::Number { + py: self.py, + number_string: None, + _types: PhantomData, + }); + } + + Ok(StructSerializer::Struct(PythonStructDictSerializer { + py: self.py, + builder: P::NamedMap::builder(self.py, len, name)?, + _types: PhantomData, + })) + } + + #[cfg(not(feature = "arbitrary_precision"))] + { + Ok(PythonStructDictSerializer { + py: self.py, + builder: P::NamedMap::builder(self.py, len, name)?, + _types: PhantomData, + }) + } } fn serialize_struct_variant( @@ -569,6 +608,58 @@ impl<'py, P: PythonizeTypes> ser::SerializeMap for PythonMapSerializer<'py, P> { } } +#[cfg(feature = "arbitrary_precision")] +impl<'py, P: PythonizeTypes> ser::SerializeStruct for StructSerializer<'py, P> { + type Ok = Bound<'py, PyAny>; + type Error = PythonizeError; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + match self { + StructSerializer::Struct(s) => s.serialize_field(key, value), + StructSerializer::Number { number_string, .. } => { + let serde_json::Value::String(s) = value + .serialize(serde_json::value::Serializer) + .map_err(|e| PythonizeError::msg(format!("Failed to serialize number: {}", e)))? + else { + return Err(PythonizeError::msg("Expected string in serde_json::Number")); + }; + + *number_string = Some(s); + Ok(()) + } + } + } + + fn end(self) -> Result> { + match self { + StructSerializer::Struct(s) => s.end(), + StructSerializer::Number { + py, number_string: Some(s), .. + } => { + if let Ok(i) = s.parse::() { + return Ok(PyInt::new(py, i).into_any()); + } + if let Ok(u) = s.parse::() { + return Ok(PyInt::new(py, u).into_any()); + } + if s.chars().any(|c| c == '.' || c == 'e' || c == 'E') { + if let Ok(f) = s.parse::() { + return Ok(PyFloat::new(py, f).into_any()); + } + } + // Fall back to Python's int() constructor, which supports arbitrary precision. + py.get_type::() + .call1((s.as_str(),)) + .map_err(|e| PythonizeError::msg(format!("Invalid number: {}", e))) + } + StructSerializer::Number { .. } => Err(PythonizeError::msg("Empty serde_json::Number")), + } + } +} + impl<'py, P: PythonizeTypes> ser::SerializeStruct for PythonStructDictSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; diff --git a/tests/test_arbitrary_precision.rs b/tests/test_arbitrary_precision.rs new file mode 100644 index 0000000..7719eaf --- /dev/null +++ b/tests/test_arbitrary_precision.rs @@ -0,0 +1,57 @@ +#![cfg(feature = "arbitrary_precision")] + +use pyo3::prelude::*; +use pythonize::pythonize; +use serde_json::Value; + +#[test] +fn test_greater_than_u64_max() { + Python::attach(|py| { + let json_str = r#"18446744073709551616"#; + let value: Value = serde_json::from_str(json_str).unwrap(); + let result = pythonize(py, &value).unwrap(); + let number_str = result.str().unwrap().to_string(); + + assert!(result.is_instance_of::()); + assert_eq!(number_str, "18446744073709551616"); + }); +} + +#[test] +fn test_less_than_i64_min() { + Python::attach(|py| { + let json_str = r#"-9223372036854775809"#; + let value: Value = serde_json::from_str(json_str).unwrap(); + let result = pythonize(py, &value).unwrap(); + let number_str = result.str().unwrap().to_string(); + + assert!(result.is_instance_of::()); + assert_eq!(number_str, "-9223372036854775809"); + }); +} + +#[test] +fn test_float() { + Python::attach(|py| { + let json_str = r#"3.141592653589793238"#; + let value: Value = serde_json::from_str(json_str).unwrap(); + let result = pythonize(py, &value).unwrap(); + let num: f32 = result.extract().unwrap(); + + assert!(result.is_instance_of::()); + assert_eq!(num, 3.141592653589793238); // not {'$serde_json::private::Number': ...} + }); +} + +#[test] +fn test_int() { + Python::attach(|py| { + let json_str = r#"2"#; + let value: Value = serde_json::from_str(json_str).unwrap(); + let result = pythonize(py, &value).unwrap(); + let num: i32 = result.extract().unwrap(); + + assert!(result.is_instance_of::()); + assert_eq!(num, 2); // not {'$serde_json::private::Number': '2'} + }); +}