Safe JSON representations with Rust
Table of contents
One of the reasons why I like serverless technologies is that code is a liability. I can push a lot of the logic to handle and process requests as possible to the cloud provider, and focus on the core business logic.
However, I am still liable for the code that I need to write. The Rust programming language is quite interesting for this, as it’s designed for highly safe systems. You might already know some of its features on the memory safety side, but it also offer other useful patterns for building safe applications.
JSON responses
For example, let’s say we are building an application that returns a JSON response to the end user. Its representation will be different based on whether we return a success or not.
If the operation is a success, we’ll return a data attribute containing the object’s properties:
However, if there is any issue, we’ll return an error error:
Simple Rust representation
To represent the response before serializing it into a JSON object, we could create a struct with all possible properties, and mark both data
and error
as optional.
use Serialize;
We’re using the serde
crate to mark our struct as serializable. This mean we’ll be able to transform it easily into a JSON object using serde_json
later.
If you’re new to Rust, you might be wondering what are those Option<>
types we use for the fields. An Option either contain some value or none. As those values depends on if the response marks a success or not, they might not be present in the response.
This is why we use skip_serializing_if
for these fields. With serde, we can prevent serializing fields based on the result of a function. If the Option is None (doesn’t contain a value), we will skip this field in the JSON object.
Happy cases
Doing like so works pretty well to represent both possibles states. We can manually transform a Result<>
from another operation into our struct.
use to_string_pretty;
Unhappy cases
However, the problem with representing our responses this way is that a developer could accidentally create a response that would be valid in Rust, but invalid from the end users.
use to_string_pretty;
Here, we have a response that’s marked as unsuccessful, contains some data and and error message. From the end-user’s perspective, this is an undefined behavior compared to the original specification.
Enum representation
We could argue that it’s up to the developer to ensure they’re writing code that will behave as expected, but can we do better? If we go back to what we want to send as responses, there are two possible states:
- The operation is a success and we return a data object; or
- The operation was unsuccessful and we return an error message.
In Rust, we can just represent this as an Enum. Contrary to many programming languages, we can use Enum to represent variants that hold data.
We have two variants here: a success that contains a Data value, and an error that contains a String (our error message). From a developer point of view, that means they can only represent valid responses.
For reference, here’s how we would map the Result<>
of the operation into a response:
use to_string_pretty;
Custom serialization
However, we’re still missing one thing. If you run the code above, you will get an error as our ConditionalFieldsResponse
enum doesn’t implement Serialize!
the serde
crate supports some enum representation patterns out of the box. In this case, we need to have a custom representation as the success
key is a boolean value.
Thankfully, we can create our own implementation of the Serialize
trait:
use ;
Sample project 🦀
If you want to see a sample implementation that you can just download and run, I’ve created a GitHub repository.