Serializing and Deserializing Arbitrary Data Into and From JsValue with Serde

It's possible to pass arbitrary data from Rust to JavaScript by serializing it with Serde. This can be done through the serde-wasm-bindgen crate.

Add dependencies

To use serde-wasm-bindgen, you first have to add it as a dependency in your Cargo.toml. You also need the serde crate, with the derive feature enabled, to allow your types to be serialized and deserialized with Serde.

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"

Derive the Serialize and Deserialize Traits

Add #[derive(Serialize, Deserialize)] to your type. All of your type's members must also be supported by Serde, i.e. their types must also implement the Serialize and Deserialize traits.

For example, let's say we'd like to pass this struct to JavaScript; doing so is not possible in wasm-bindgen normally due to the use of HashMaps, arrays, and nested Vecs. None of those types are supported for sending across the wasm ABI naively, but all of them implement Serde's Serialize and Deserialize.

Note that we do not need to use the #[wasm_bindgen] macro.

#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
pub struct Example {
    pub field1: HashMap<u32, String>,
    pub field2: Vec<Vec<f32>>,
    pub field3: [f32; 4],
}
}

Send it to JavaScript with serde_wasm_bindgen::to_value

Here's a function that will pass an Example to JavaScript by serializing it to JsValue:

#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub fn send_example_to_js() -> JsValue {
    let mut field1 = HashMap::new();
    field1.insert(0, String::from("ex"));
    let example = Example {
        field1,
        field2: vec![vec![1., 2.], vec![3., 4.]],
        field3: [1., 2., 3., 4.]
    };

    serde_wasm_bindgen::to_value(&example).unwrap()
}
}

Receive it from JavaScript with serde_wasm_bindgen::from_value

Here's a function that will receive a JsValue parameter from JavaScript and then deserialize an Example from it:

#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub fn receive_example_from_js(val: JsValue) {
    let example: Example = serde_wasm_bindgen::from_value(val).unwrap();
    ...
}
}

JavaScript Usage

In the JsValue that JavaScript gets, field1 will be a Map, field2 will be a JavaScript Array whose members are Arrays of numbers, and field3 will be an Array of numbers.

import { send_example_to_js, receive_example_from_js } from "example";

// Get the example object from wasm.
let example = send_example_to_js();

// Add another "Vec" element to the end of the "Vec<Vec<f32>>"
example.field2.push([5, 6]);

// Send the example object back to wasm.
receive_example_from_js(example);

An alternative approach - using JSON

serde-wasm-bindgen works by directly manipulating JavaScript values. This requires a lot of calls back and forth between Rust and JavaScript, which can sometimes be slow. An alternative way of doing this is to serialize values to JSON, and then parse them on the other end. Browsers' JSON implementations are usually quite fast, and so this approach can outstrip serde-wasm-bindgen's performance in some cases. But this approach supports only types that can be serialized as JSON, leaving out some important types that serde-wasm-bindgen supports such as Map, Set, and array buffers.

That's not to say that using JSON is always faster, though - the JSON approach can be anywhere from 2x to 0.2x the speed of serde-wasm-bindgen, depending on the JS runtime and the values being passed. It also leads to larger code size than serde-wasm-bindgen. So, make sure to profile each for your own use cases.

This approach is implemented in gloo_utils::format::JsValueSerdeExt:

# Cargo.toml
[dependencies]
gloo-utils = { version = "0.1", features = ["serde"] }
#![allow(unused)]
fn main() {
use gloo_utils::format::JsValueSerdeExt;

#[wasm_bindgen]
pub fn send_example_to_js() -> JsValue {
    let mut field1 = HashMap::new();
    field1.insert(0, String::from("ex"));
    let example = Example {
        field1,
        field2: vec![vec![1., 2.], vec![3., 4.]],
        field3: [1., 2., 3., 4.]
    };

    JsValue::from_serde(&example).unwrap()
}

#[wasm_bindgen]
pub fn receive_example_from_js(val: JsValue) {
    let example: Example = val.into_serde().unwrap();
    ...
}
}

History

In previous versions of wasm-bindgen, gloo-utils's JSON-based Serde support (JsValue::from_serde and JsValue::into_serde) was built into wasm-bindgen itself. However, this required a dependency on serde_json, which had a problem: with certain features of serde_json and other crates enabled, serde_json would end up with a circular dependency on wasm-bindgen, which is illegal in Rust and caused people's code to fail to compile. So, these methods were extracted out into gloo-utils with an extension trait and the originals were deprecated.