Skip to content

Commit a444b75

Browse files
feat: Support serde_json's arbitrary_precision feature (#102)
* feat: Support serde_json's arbitrary_precision feature * docs: Describe arbitrary_precision feature in readme. Update changelog. ci: Run tests for arbitrary_precision feature. * fmt * opt out of default serde-json features --------- Co-authored-by: David Hewitt <mail@davidhewitt.dev>
1 parent d22654a commit a444b75

File tree

7 files changed

+276
-15
lines changed

7 files changed

+276
-15
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ jobs:
9494
- name: Test (abi3)
9595
run: cargo test --verbose --features pyo3/abi3-py37
9696

97+
- name: Test (arbitrary_precision)
98+
run: cargo test --verbose --features arbitrary_precision
99+
97100
env:
98101
RUST_BACKTRACE: 1
99102

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Bump MSRV to 1.83.
44
- Update `pyo3` to 0.28.
5+
- Add `arbitrary_precision` feature
56

67
## 0.27.0 - 2025-11-07
78
- Update to PyO3 0.27

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ documentation = "https://docs.rs/crate/pythonize/"
1313

1414
[dependencies]
1515
serde = { version = "1.0", default-features = false, features = ["std"] }
16+
serde_json = { version = "1.0", optional = true, default-features = false, features = ["std"] }
1617
pyo3 = { version = "0.28", default-features = false }
1718

1819
[dev-dependencies]
1920
serde = { version = "1.0", default-features = false, features = ["derive"] }
2021
pyo3 = { version = "0.28", default-features = false, features = ["auto-initialize", "macros", "py-clone"] }
21-
serde_json = "1.0"
22+
serde_json = { version = "1.0", default-features = false, features = ["std"] }
2223
serde_bytes = "0.11"
2324
maplit = "1.0.2"
2425
serde_path_to_error = "0.1.15"
26+
27+
[features]
28+
arbitrary_precision = ["serde_json", "serde_json/arbitrary_precision"]

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Pythonize has two main public APIs: `pythonize` and `depythonize`.
1717
[Serde]: https://github.com/serde-rs/serde
1818
[PyO3]: https://github.com/PyO3/pyo3
1919

20-
# Examples
20+
## Examples
2121

2222
```rust
2323
use serde::{Serialize, Deserialize};
@@ -47,3 +47,14 @@ Python::attach(|py| {
4747
assert_eq!(new_sample, sample);
4848
})
4949
```
50+
51+
## Features
52+
53+
### `arbitrary_precision`
54+
55+
Enable support for `serde_json`'s `arbitrary_precision` feature, which allows handling numbers that exceed the range of `i128`/`u128` when converting `serde_json::Value` to and from Python.
56+
57+
```toml
58+
[dependencies]
59+
pythonize = { version = "0.28", features = ["arbitrary_precision"] }
60+
```

src/de.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ use serde::Deserialize;
44

55
use crate::error::{ErrorImpl, PythonizeError, Result};
66

7+
#[cfg(feature = "arbitrary_precision")]
8+
const TOKEN: &str = "$serde_json::private::Number";
9+
710
/// Attempt to convert a Python object to an instance of `T`
811
pub fn depythonize<'a, 'py, T>(obj: &'a Bound<'py, PyAny>) -> Result<T>
912
where
@@ -68,8 +71,7 @@ impl<'a, 'py> Depythonizer<'a, 'py> {
6871
} else {
6972
visitor.visit_u128(x)
7073
}
71-
} else {
72-
let x: i128 = int.extract()?;
74+
} else if let Ok(x) = int.extract::<i128>() {
7375
if let Ok(x) = i8::try_from(x) {
7476
visitor.visit_i8(x)
7577
} else if let Ok(x) = i16::try_from(x) {
@@ -81,6 +83,19 @@ impl<'a, 'py> Depythonizer<'a, 'py> {
8183
} else {
8284
visitor.visit_i128(x)
8385
}
86+
} else {
87+
#[cfg(feature = "arbitrary_precision")]
88+
{
89+
visitor.visit_map(NumberDeserializer {
90+
number: Some(int.to_string()),
91+
})
92+
}
93+
#[cfg(not(feature = "arbitrary_precision"))]
94+
{
95+
// Re-attempt to return the original error.
96+
let _: i128 = int.extract()?;
97+
unreachable!()
98+
}
8499
}
85100
}
86101
}
@@ -513,6 +528,34 @@ impl<'de> de::VariantAccess<'de> for PyEnumAccess<'_, '_> {
513528
}
514529
}
515530

531+
// See serde_json
532+
#[cfg(feature = "arbitrary_precision")]
533+
struct NumberDeserializer {
534+
number: Option<String>,
535+
}
536+
537+
#[cfg(feature = "arbitrary_precision")]
538+
impl<'de> de::MapAccess<'de> for NumberDeserializer {
539+
type Error = PythonizeError;
540+
541+
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
542+
where
543+
K: de::DeserializeSeed<'de>,
544+
{
545+
if self.number.is_none() {
546+
return Ok(None);
547+
}
548+
seed.deserialize(TOKEN.into_deserializer()).map(Some)
549+
}
550+
551+
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
552+
where
553+
V: de::DeserializeSeed<'de>,
554+
{
555+
seed.deserialize(self.number.take().unwrap().into_deserializer())
556+
}
557+
}
558+
516559
#[cfg(test)]
517560
mod test {
518561
use std::ffi::CStr;

src/ser.rs

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::marker::PhantomData;
22

3+
#[cfg(feature = "arbitrary_precision")]
4+
use pyo3::types::{PyAnyMethods, PyFloat, PyInt};
35
use pyo3::types::{
46
PyDict, PyDictMethods, PyList, PyListMethods, PyMapping, PySequence, PyString, PyTuple,
57
PyTupleMethods,
@@ -229,6 +231,21 @@ pub struct PythonStructVariantSerializer<'py, P: PythonizeTypes> {
229231
inner: PythonStructDictSerializer<'py, P>,
230232
}
231233

234+
#[cfg(feature = "arbitrary_precision")]
235+
#[doc(hidden)]
236+
pub enum StructSerializer<'py, P: PythonizeTypes> {
237+
Struct(PythonStructDictSerializer<'py, P>),
238+
Number {
239+
py: Python<'py>,
240+
number_string: Option<String>,
241+
_types: PhantomData<P>,
242+
},
243+
}
244+
245+
#[cfg(not(feature = "arbitrary_precision"))]
246+
#[doc(hidden)]
247+
pub type StructSerializer<'py, P> = PythonStructDictSerializer<'py, P>;
248+
232249
#[doc(hidden)]
233250
pub struct PythonStructDictSerializer<'py, P: PythonizeTypes> {
234251
py: Python<'py>,
@@ -266,7 +283,7 @@ impl<'py, P: PythonizeTypes> ser::Serializer for Pythonizer<'py, P> {
266283
type SerializeTupleStruct = PythonCollectionSerializer<'py, P>;
267284
type SerializeTupleVariant = PythonTupleVariantSerializer<'py, P>;
268285
type SerializeMap = PythonMapSerializer<'py, P>;
269-
type SerializeStruct = PythonStructDictSerializer<'py, P>;
286+
type SerializeStruct = StructSerializer<'py, P>;
270287
type SerializeStructVariant = PythonStructVariantSerializer<'py, P>;
271288

272289
fn serialize_bool(self, v: bool) -> Result<Bound<'py, PyAny>> {
@@ -435,16 +452,34 @@ impl<'py, P: PythonizeTypes> ser::Serializer for Pythonizer<'py, P> {
435452
})
436453
}
437454

438-
fn serialize_struct(
439-
self,
440-
name: &'static str,
441-
len: usize,
442-
) -> Result<PythonStructDictSerializer<'py, P>> {
443-
Ok(PythonStructDictSerializer {
444-
py: self.py,
445-
builder: P::NamedMap::builder(self.py, len, name)?,
446-
_types: PhantomData,
447-
})
455+
fn serialize_struct(self, name: &'static str, len: usize) -> Result<StructSerializer<'py, P>> {
456+
#[cfg(feature = "arbitrary_precision")]
457+
{
458+
// With arbitrary_precision enabled, a serde_json::Number serializes as a "$serde_json::private::Number"
459+
// struct with a "$serde_json::private::Number" field, whose value is the String in Number::n.
460+
if name == "$serde_json::private::Number" && len == 1 {
461+
return Ok(StructSerializer::Number {
462+
py: self.py,
463+
number_string: None,
464+
_types: PhantomData,
465+
});
466+
}
467+
468+
Ok(StructSerializer::Struct(PythonStructDictSerializer {
469+
py: self.py,
470+
builder: P::NamedMap::builder(self.py, len, name)?,
471+
_types: PhantomData,
472+
}))
473+
}
474+
475+
#[cfg(not(feature = "arbitrary_precision"))]
476+
{
477+
Ok(PythonStructDictSerializer {
478+
py: self.py,
479+
builder: P::NamedMap::builder(self.py, len, name)?,
480+
_types: PhantomData,
481+
})
482+
}
448483
}
449484

450485
fn serialize_struct_variant(
@@ -569,6 +604,62 @@ impl<'py, P: PythonizeTypes> ser::SerializeMap for PythonMapSerializer<'py, P> {
569604
}
570605
}
571606

607+
#[cfg(feature = "arbitrary_precision")]
608+
impl<'py, P: PythonizeTypes> ser::SerializeStruct for StructSerializer<'py, P> {
609+
type Ok = Bound<'py, PyAny>;
610+
type Error = PythonizeError;
611+
612+
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
613+
where
614+
T: ?Sized + Serialize,
615+
{
616+
match self {
617+
StructSerializer::Struct(s) => s.serialize_field(key, value),
618+
StructSerializer::Number { number_string, .. } => {
619+
let serde_json::Value::String(s) = value
620+
.serialize(serde_json::value::Serializer)
621+
.map_err(|e| {
622+
PythonizeError::msg(format!("Failed to serialize number: {}", e))
623+
})?
624+
else {
625+
return Err(PythonizeError::msg("Expected string in serde_json::Number"));
626+
};
627+
628+
*number_string = Some(s);
629+
Ok(())
630+
}
631+
}
632+
}
633+
634+
fn end(self) -> Result<Bound<'py, PyAny>> {
635+
match self {
636+
StructSerializer::Struct(s) => s.end(),
637+
StructSerializer::Number {
638+
py,
639+
number_string: Some(s),
640+
..
641+
} => {
642+
if let Ok(i) = s.parse::<i64>() {
643+
return Ok(PyInt::new(py, i).into_any());
644+
}
645+
if let Ok(u) = s.parse::<u64>() {
646+
return Ok(PyInt::new(py, u).into_any());
647+
}
648+
if s.chars().any(|c| c == '.' || c == 'e' || c == 'E') {
649+
if let Ok(f) = s.parse::<f64>() {
650+
return Ok(PyFloat::new(py, f).into_any());
651+
}
652+
}
653+
// Fall back to Python's int() constructor, which supports arbitrary precision.
654+
py.get_type::<PyInt>()
655+
.call1((s.as_str(),))
656+
.map_err(|e| PythonizeError::msg(format!("Invalid number: {}", e)))
657+
}
658+
StructSerializer::Number { .. } => Err(PythonizeError::msg("Empty serde_json::Number")),
659+
}
660+
}
661+
}
662+
572663
impl<'py, P: PythonizeTypes> ser::SerializeStruct for PythonStructDictSerializer<'py, P> {
573664
type Ok = Bound<'py, PyAny>;
574665
type Error = PythonizeError;

tests/test_arbitrary_precision.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#![cfg(feature = "arbitrary_precision")]
2+
3+
use pyo3::prelude::*;
4+
use pythonize::{depythonize, pythonize};
5+
use serde_json::Value;
6+
7+
#[test]
8+
fn test_greater_than_u64_max() {
9+
Python::attach(|py| {
10+
let json_str = r#"18446744073709551616"#;
11+
let value: Value = serde_json::from_str(json_str).unwrap();
12+
let result = pythonize(py, &value).unwrap();
13+
let number_str = result.str().unwrap().to_string();
14+
15+
assert!(result.is_instance_of::<pyo3::types::PyInt>());
16+
assert_eq!(number_str, "18446744073709551616");
17+
});
18+
}
19+
20+
#[test]
21+
fn test_less_than_i64_min() {
22+
Python::attach(|py| {
23+
let json_str = r#"-9223372036854775809"#;
24+
let value: Value = serde_json::from_str(json_str).unwrap();
25+
let result = pythonize(py, &value).unwrap();
26+
let number_str = result.str().unwrap().to_string();
27+
28+
assert!(result.is_instance_of::<pyo3::types::PyInt>());
29+
assert_eq!(number_str, "-9223372036854775809");
30+
});
31+
}
32+
33+
#[test]
34+
fn test_float() {
35+
Python::attach(|py| {
36+
let json_str = r#"3.141592653589793238"#;
37+
let value: Value = serde_json::from_str(json_str).unwrap();
38+
let result = pythonize(py, &value).unwrap();
39+
let num: f32 = result.extract().unwrap();
40+
41+
assert!(result.is_instance_of::<pyo3::types::PyFloat>());
42+
assert_eq!(num, 3.141592653589793238); // not {'$serde_json::private::Number': ...}
43+
});
44+
}
45+
46+
#[test]
47+
fn test_int() {
48+
Python::attach(|py| {
49+
let json_str = r#"2"#;
50+
let value: Value = serde_json::from_str(json_str).unwrap();
51+
let result = pythonize(py, &value).unwrap();
52+
let num: i32 = result.extract().unwrap();
53+
54+
assert!(result.is_instance_of::<pyo3::types::PyInt>());
55+
assert_eq!(num, 2); // not {'$serde_json::private::Number': '2'}
56+
});
57+
}
58+
59+
#[test]
60+
fn test_serde_error_if_token_empty() {
61+
let json_str = r#"{"$serde_json::private::Number": ""}"#;
62+
let result: Result<Value, _> = serde_json::from_str(json_str);
63+
64+
assert!(result.is_err());
65+
assert!(result
66+
.unwrap_err()
67+
.to_string()
68+
.contains("EOF while parsing a value"));
69+
}
70+
71+
#[test]
72+
fn test_serde_error_if_token_invalid() {
73+
let json_str = r#"{"$serde_json::private::Number": 2}"#;
74+
let result: Result<Value, _> = serde_json::from_str(json_str);
75+
76+
assert!(result.is_err());
77+
assert!(result
78+
.unwrap_err()
79+
.to_string()
80+
.contains("invalid type: integer `2`, expected string containing a number"));
81+
}
82+
83+
#[test]
84+
fn test_token_valid() {
85+
Python::attach(|py| {
86+
let json_str = r#"{"$serde_json::private::Number": "2"}"#;
87+
let value: Value = serde_json::from_str(json_str).unwrap();
88+
let result = pythonize(py, &value).unwrap();
89+
let num: i32 = result.extract().unwrap();
90+
91+
assert!(result.is_instance_of::<pyo3::types::PyInt>());
92+
assert_eq!(num, 2);
93+
});
94+
}
95+
96+
#[test]
97+
fn test_depythonize_greater_than_u128_max() {
98+
Python::attach(|py| {
99+
// u128::MAX + 1
100+
let py_int = py
101+
.eval(c"340282366920938463463374607431768211456", None, None)
102+
.unwrap();
103+
let value: Value = depythonize(&py_int).unwrap();
104+
105+
assert!(value.is_number());
106+
assert_eq!(value.to_string(), "340282366920938463463374607431768211456");
107+
});
108+
}

0 commit comments

Comments
 (0)