use pyo3::prelude::*;
use pyo3::types::{PyBool, PyBytes, PyDict, PyFloat, PyInt, PyList, PyString, PyTuple};
use serde_json::{Map, Value};
pub fn value_to_pyobject<'py>(py: Python<'py>, value: &Value) -> PyResult<Bound<'py, PyAny>> {
match value {
Value::Null => Ok(py.None().into_bound(py)),
Value::Bool(b) => Ok(PyBool::new(py, *b).to_owned().into_any()),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(i.into_pyobject(py)?.into_any())
} else if let Some(u) = n.as_u64() {
Ok(u.into_pyobject(py)?.into_any())
} else {
let f = n.as_f64().ok_or_else(|| {
pyo3::exceptions::PyValueError::new_err("non-finite number in JSON value")
})?;
Ok(f.into_pyobject(py)?.into_any())
}
}
Value::String(s) => Ok(PyString::new(py, s).into_any()),
Value::Array(items) => {
let list = PyList::empty(py);
for item in items {
list.append(value_to_pyobject(py, item)?)?;
}
Ok(list.into_any())
}
Value::Object(map) => {
let dict = PyDict::new(py);
for (k, v) in map {
dict.set_item(k, value_to_pyobject(py, v)?)?;
}
Ok(dict.into_any())
}
}
}
pub fn pyobject_to_value(obj: &Bound<'_, PyAny>) -> PyResult<Value> {
if obj.is_none() {
return Ok(Value::Null);
}
if let Ok(b) = obj.downcast::<PyBool>() {
return Ok(Value::Bool(b.is_true()));
}
if let Ok(s) = obj.downcast::<PyString>() {
return Ok(Value::String(s.extract()?));
}
if let Ok(b) = obj.downcast::<PyBytes>() {
let bytes: &[u8] = b.as_bytes();
let arr: Vec<Value> = bytes
.iter()
.map(|x| Value::Number((*x as i64).into()))
.collect();
return Ok(Value::Array(arr));
}
if let Ok(_f) = obj.downcast::<PyFloat>() {
let f: f64 = obj.extract()?;
let n = serde_json::Number::from_f64(f).ok_or_else(|| {
pyo3::exceptions::PyValueError::new_err("non-finite float cannot be encoded to JSON")
})?;
return Ok(Value::Number(n));
}
if let Ok(_i) = obj.downcast::<PyInt>() {
if let Ok(i) = obj.extract::<i64>() {
return Ok(Value::Number(i.into()));
}
if let Ok(u) = obj.extract::<u64>() {
return Ok(Value::Number(u.into()));
}
let f: f64 = obj.extract()?;
let n = serde_json::Number::from_f64(f)
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("integer too large for JSON"))?;
return Ok(Value::Number(n));
}
if let Ok(list) = obj.downcast::<PyList>() {
let mut out = Vec::with_capacity(list.len());
for item in list.iter() {
out.push(pyobject_to_value(&item)?);
}
return Ok(Value::Array(out));
}
if let Ok(tuple) = obj.downcast::<PyTuple>() {
let mut out = Vec::with_capacity(tuple.len());
for item in tuple.iter() {
out.push(pyobject_to_value(&item)?);
}
return Ok(Value::Array(out));
}
if let Ok(dict) = obj.downcast::<PyDict>() {
let mut map = Map::new();
for (k, v) in dict.iter() {
let key: String = k
.extract()
.or_else(|_| -> PyResult<String> { k.str()?.extract() })?;
map.insert(key, pyobject_to_value(&v)?);
}
return Ok(Value::Object(map));
}
let s: String = obj.str()?.extract()?;
Ok(Value::String(s))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn roundtrip_primitives() {
crate::ensure_initialized();
Python::with_gil(|py| {
for v in [
json!(null),
json!(true),
json!(42i64),
json!(3.5f64),
json!("hello"),
json!([1, "two", false]),
json!({"a": 1, "b": [2, 3]}),
] {
let py_obj = value_to_pyobject(py, &v).unwrap();
let back = pyobject_to_value(&py_obj).unwrap();
assert_eq!(back, v, "round-trip failed for {v:?}");
}
});
}
}