Feedback on using serde_json with Rc and RefCell

I started to use Rust to reimplement a Python program. I need to read and write JSON files containing objects, access and mutate these objects and pass them as function parameters and return values without making copies of the objects basically like Python object references. I'm new to Rust so I would appreciate feedback on my approach before I do the full reimplementation.

To present my approach I made a simple example program. Here the objects are Spaces and Coordinates. The full package is in ruostetsaari / rcserde, and below is all Rust code (main.rs).

Is this a good approach? Is there a more Rustacean way to proceed?

// Cargo.toml:
// [dependencies]
// serde = { version = "1.0", features = ["derive", "rc"] }
// serde_json = "1.0"
use serde::{Serialize, Deserialize};
use serde_json;
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct Coordinate {
    name: String,
    ctype: String,
}

type RCoordinate = Rc<RefCell<Coordinate>>;

#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct Space {
    name: String,
    coordinates: Vec<RCoordinate>,
}

type RSpace = Rc<RefCell<Space>>;

// Note! Error handling ignored in these functions to shorten the code

fn parse_space(json_str: &str) -> RSpace {
    Rc::new(RefCell::new(serde_json::from_str(&json_str).unwrap()))
}

fn get_coordinate(space: &RSpace, index: usize) -> RCoordinate {
    space.borrow().coordinates[index].clone()
}

fn main() {
    let input_str = r#"
	{
	    "name": "2D",
	    "coordinates": [
		{
		    "name": "x",
		    "ctype": "float"
		},
		{
		    "name": "y",
		    "ctype": "float"
		}
	    ]
	}"#;
    let space1 = parse_space(&input_str);
    println!("space1 {:?}", &space1);
    let space2 = parse_space(&serde_json::to_string(&space1).unwrap());
    println!("space2 {:?}", &space2);
    assert_eq!(space1, space2);
    let space3 = space2.clone();
    space3.borrow_mut().name = "3D".to_string();
    space3.borrow_mut().coordinates.push(Rc::new(RefCell::new(Coordinate { name: "z".to_string(), ctype: "float".to_string() })));
    println!("space3 {:?}", &space3);
    println!("space2 {:?}", &space2);
    assert_eq!(space2, space3);
    println!("z_coordinate {:?}", &get_coordinate(&space2, 2));
}
1 Like

There might be, except for your constraint:

Given the above, I think this is a reasonable fit.

Though not without a caveat: the Rc/RefCell approach is not particularly CPU-cache friendly and so will incur a performance penalty relative to giving each object a single owner.

Thanks! I realize the performance penalty. Luckily the JSON object handling is used in the beginning and in the of the processing and the computational heavy lifting will be done with stack allocated variables and will be much faster than with Python. That is one reason why we are switching from Python to Rust.

Keep it in mind that unlike python object all the Rc does is simple reference counting. Cyclic references would leak the allocation. Though it may not be a problem if it's not a long running server.

Also, since you've enabled the rc feature of the serde, you can replace this line

let space1 = parse_space(&input_str);

with this.

let space1: RSpace = serde_json::from_str(&input_str).unwrap();
1 Like

Thanks! This avoids some unnecessary Rc::new(RefCell::new(..)) boilerplate.

The program never generates cyclic references, so that won't be a problem here.