blob: fba113b0689ac6687a1000b3ea0efbd496c0602f [file] [log] [blame] [view]
# 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](https://github.com/serde-rs/serde). This can be done through the
[`serde-wasm-bindgen`](https://docs.rs/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.
```toml
[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.
```rust
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`:
```rust
#[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:
```rust
#[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.
```js
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`]:
```toml
# Cargo.toml
[dependencies]
gloo-utils = { version = "0.1", features = ["serde"] }
```
```rust
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();
...
}
```
[`gloo_utils::format::JsValueSerdeExt`]: https://docs.rs/gloo-utils/latest/gloo_utils/format/trait.JsValueSerdeExt.html
## 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.