Serializing and Deserializing Arbitrary Data Into and From JsValue
with Serde
It's possible to pass arbirtrary data from Rust to JavaScript by serializing it
to JSON with Serde. wasm-bindgen
includes
the JsValue
type, which streamlines serializing and deserializing.
Enable the "serde-serialize"
Feature
To enable the "serde-serialize"
feature, do two things in Cargo.toml
:
- Add the
serde
andserde_derive
crates to[dependencies]
. - Add
features = ["serde-serialize"]
to the existingwasm-bindgen
dependency.
[dependencies]
serde = "^1.0.59"
serde_derive = "^1.0.59"
[dependencies.wasm-bindgen]
version = "^0.2"
features = ["serde-serialize"]
Import Serde's Custom-Derive Macros
In your top-level Rust file (e.g. lib.rs
or main.rs
), enable the Serialize
and Deserialize
custom-derive macros:
# #![allow(unused_variables)] #fn main() { #[macro_use] extern crate serde_derive; #}
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_variables)] #fn main() { #[derive(Serialize)] pub struct Example { pub field1: HashMap<u32, String>, pub field2: Vec<Vec<f32>>, pub field3: [f32; 4], } #}
Send it to JavaScript with JsValue::from_serde
Here's a function that will pass an Example
to JavaScript by serializing it to
JsValue
:
# #![allow(unused_variables)] #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.] }; JsValue::from_serde(&example).unwrap() } #}
Receive it from JavaScript with JsValue::into_serde
Here's a function that will receive a JsValue
parameter from JavaScript and
then deserialize an Example
from it:
# #![allow(unused_variables)] #fn main() { #[wasm_bindgen] pub fn receive_example_from_js(val: &JsValue) { let example: Example = val.into_serde().unwrap(); ... } #}
JavaScript Usage
In the JsValue
that JavaScript gets, field1
will be an Object
(not a
JavaScript 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: serde-wasm-bindgen
The serde-wasm-bindgen
crate serializes and
deserializes Rust structures directly to JsValue
s, without going through
temporary JSON stringification. This approach has both advantages and
disadvantages.
The primary advantage is smaller code size: going through JSON entrenches code
to stringify and parse floating point numbers, which is not a small amount of
code. It also supports more types than JSON does, such as Map
, Set
, and
array buffers.
There are two primary disadvantages. The first is that it is not always compatible with the default JSON-based serialization. The second is that it performs more calls back and forth between JS and Wasm, which has not been fully optimized in all engines, meaning it can sometimes be a speed regression. However, in other cases, it is a speed up over the JSON-based stringification, so — as always — make sure to profile your own use cases as necessary.