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 HashMap
s, arrays,
and nested Vec
s. 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 Array
s 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.