For storing data that is more complex than that which is stored in a configuration file, JSON format is more appropriate. This format is quite popular, particularly among those who use the JavaScript language.
We are going to read and parse the data/sales.json file. This file contains a single anonymous object, which contains two arrays—"products" and "sales".
The "products" array contains two objects, each one having three fields:
"products": [
{
"id": 591,
"category": "fruit",
"name": "orange"
},
{
"id": 190,
"category": "furniture",
"name": "chair"
}
],
The "sales" array contains three objects, each one containing five fields:
"sales": [
{
"id": "2020-7110",
"product_id": 190,
"date": 1234527890,
"quantity": 2.0,
"unit": "u."
},
{
"id": "2020-2871",
"product_id": 591,
"date": 1234567590,
"quantity": 2.14,
"unit": "Kg"
},
{
"id": "2020-2583",
"product_id": 190,
"date": 1234563890,
"quantity": 4.0,
"unit": "u."
}
]
The information in the arrays is about some products to sell and some sale transactions associated with those products. Notice that the second field of each sale ("product_id") is a reference to a product, and so it should be processed after the corresponding product object has been created.
We will see a pair of programs with the same behavior. They read the JSON file, increment the quantity of the second sale object by 1.5, and then save the whole updated structure into another JSON file.
Similarly to the TOML format case, there can also be a dynamic parsing technique used for JSON files, where the existence and type of any data field is checked by the application code, and a static parsing technique, where it uses the deserialization library to check the existence and type of any field.
So, we have two projects: json_dynamic and json_static. To run each of them, open its folder and type in cargo run ../data/sales.json ../data/sales2.json. The program will not print anything, but it will read the first file specified in the command line and create the second file that is specified.
The created file is similar to the read file, but with the following differences:
- The fields of the file created by json_dynamic are sorted in alphabetical order, while the fields of the file created by json_static are sorted in the same order as in the Rust data structure.
- The quantity of the second sale is incremented from 2.14 to 3.64.
- The final empty line is removed in both created files.
Now, we can see the implementations of the two techniques of serialization and deserialization.
The json_dynamic project
Let's look at the source code of the project:
- This project gets the pathnames of two files from the command line—the existing JSON file ("input_path") to read into a memory structure and a JSON file to create ("output_path") by saving the loaded structure, after having modified it a bit.
- Then, the input file is loaded into the string named sales_and_products_text and the generic serde_json::from_str::<Value> function is used to parse the string into a dynamically typed structure representing the JSON file. This structure is stored in the sales_and_products local variable.
Imagine that we want to change the quantity sold by the second sale transaction, incrementing it by 1.5 kilograms:
- First, we must get to this value using the following expression:
sales_and_products["sales"][1]["quantity"]
- This retrieves the "sales" sub-object of the general object. It is an array containing three objects.
- Then, this expression gets the second item (starting from zero ([1])) of this array. This is an object representing a single sale transaction.
- After this, it gets the "quantity" sub-object of the sale transaction object.
- The value we have reached has a dynamic type that we think should be serde_json::Value::Number, and so we make a pattern matching with this type, specifying the if let Value::Number(n) clause.
- If all is good, the matching succeeds and we get a variable named n—containing a number, or something that can be converted into a Rust floating-point number by using the as_f64 function. Lastly, we can increment the Rust number and then create a JSON number from it using the from_f64 function. We can then assign this object to the JSON structure using the same expression we used to get it:
sales_and_products["sales"][1]["quantity"]
= Value::Number(Number::from_f64(
n.as_f64().unwrap() + 1.5).unwrap());
- The last statement of the program saves the JSON structure to a file. Here, the serde_json::to_string_pretty function is used. As the name suggests, this function adds formatting whitespace (blanks and new lines) to make the resulting JSON file more human-readable. There is also the serde_json::to_string function, which creates a more compact version of the same information. It is much harder for people to read, but it is somewhat quicker to process for a computer:
std::fs::write(
output_path,
serde_json::to_string_pretty(&sales_and_products).unwrap(),
).unwrap();
The json_static project
If, for our program, we are sure that we know the structure of the JSON file, a statically typed technique can and should be used instead. It is shown in the json_static project. The situation here is similar to that of the projects processing the TOML file.
The source code of the static version first declares three structs—one for every object type contained in the JSON file we are going to process. Each struct is preceded by the following attribute:
#[derive(Deserialize, Serialize, Debug)]
- The Deserialize trait is required to parse (that is, read) JSON strings into this struct.
- The Serialize trait is required to format (that is, write) this struct into a JSON string.
- The Debug trait is just handy for printing this struct on a debug trace.
The JSON string is parsed using the serde_json::from_str::<SalesAndProducts> function. Then, the code to increment the quantity of sold oranges becomes quite simple:
sales_and_products.sales[1].quantity += 1.5
The rest of the is unchanged.