February 2026

Using JSON Data in a Rust WebAssembly/wasm Module

Beginner Deck

I'm working on a toolmtg to make Magic: The Gathering Commander decks. It works okay if you send in 100 cards. It falls over if you give it 1,000 to sort through. A bummer, as that's the entire point.

I used performance.now()pn to measure times for adjusting individual cards. They average at 1ms per when there's 100. Updates jump to 30-100ms each when there's a thousand. Net result: things lock up. Then, they time out.

The page doesn't do that much, it's mainly updating data-* attributes that CSS uses to position and highlight things. The CSS was fine with a thousand cards in an earlier iteration. I know it can handle it. That points to the calculations for making changes as the problem.

The calculations are done in JavaScript. I could dig in and optimize them. Or, I could use this as an opportunity to learn a little about how wasm works. There's a chance that if use the same approach, but in rust, it'll have the speed I need. Me being me, I'm going with that.

First thing to figure out was how to get wasm to work at all. Took a little poking, but I got it goinginit. Next up: figuring out how to ingest and use the data.

Show Me The Data

The source data comes from Archidektdekt. It's a big ol' json from their APIapi. The goal is to ingest it, refine it, export the updates, then push them back to Archidekt. So, I'm looking at basic CRUDcrud. I created the following rust prototype to figure out the pipeline.

./Cargo.toml
[package]
name = "wasm_json_data_parser"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
once_cell = "1.21.3"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
wasm-bindgen = "0.2.108"
web-sys = { version = "0.3.85", features = ["console"] }
./src/lib.rs
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use wasm_bindgen::prelude::*;
use web_sys::console;

static GLOBAL_KNOWLEDGE: Lazy<Mutex<Knowledge>> =
  Lazy::new(|| Mutex::new(Knowledge::new()));

#[derive(Clone, Debug)]
pub struct Knowledge {
  data: Option<Data>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Data {
  current_index: usize,
  items: Vec<String>,
}

impl Default for Knowledge {
  fn default() -> Self {
    Self::new()
  }
}

impl Knowledge {
  pub fn new() -> Self {
    Knowledge { data: None }
  }

  pub fn change_item(&mut self) {
    self.data.as_mut().unwrap().current_index += 1;
    if self.data.as_ref().unwrap().current_index
      >= self.data.as_ref().unwrap().items.len()
    {
      self.data.as_mut().unwrap().current_index = 0
    }
  }

  pub fn current_index(&self) -> usize {
    self.data.as_ref().unwrap().current_index
  }

  pub fn current_item(&self) -> String {
    self.data.as_ref().unwrap().items[self.current_index()]
      .clone()
  }

  pub fn dump_json(&self) -> String {
    serde_json::to_string_pretty(self.data.as_ref().unwrap())
      .unwrap()
  }

  pub fn load_json(
    &mut self,
    content: String,
  ) {
    self.data = Some(serde_json::from_str(&content).unwrap())
  }
}

#[wasm_bindgen]
pub struct Oracle;

#[wasm_bindgen]
impl Oracle {
  pub fn change_item() -> Result<(), JsValue> {
    GLOBAL_KNOWLEDGE
      .lock()
      .map_err(|_| JsValue::from_str("could not get data lock"))?
      .change_item();
    Ok(())
  }

  pub fn current_index() -> Result<usize, JsValue> {
    Ok(
      GLOBAL_KNOWLEDGE
        .lock()
        .map_err(|_| {
          JsValue::from_str("could not get data lock")
        })?
        .current_index(),
    )
  }

  pub fn current_item() -> Result<String, JsValue> {
    Ok(
      GLOBAL_KNOWLEDGE
        .lock()
        .map_err(|_| {
          JsValue::from_str("could not get data lock")
        })?
        .current_item(),
    )
  }

  pub fn dump_json() -> Result<String, JsValue> {
    Ok(
      GLOBAL_KNOWLEDGE
        .lock()
        .map_err(|_| {
          JsValue::from_str("could not get data lock")
        })?
        .dump_json(),
    )
  }

  pub fn load_json(content: String) -> Result<(), JsValue> {
    console::log_1(&"Loading JSON".into());
    GLOBAL_KNOWLEDGE
      .lock()
      .map_err(|_| JsValue::from_str("could not get data lock"))?
      .load_json(content);
    Ok(())
  }
}
./index.html
<!doctype html>
<html lang="en">
<body>
    <button class="itemButton">click me</button>
    <div>
        <textarea class="jsonDump" cols="50" rows="8"></textarea>
    </div>
    <script type="module">
        import init, { Oracle } from "./pkg/wasm_json_data_parser.js"

        const jsonString = `{ 
            "current_index": 0,
            "items": ["alfa", "bravo", "charlie"] 
        }`;

        init().then(() => {
            Oracle.load_json(jsonString);
            document.querySelector(".itemButton")
                .addEventListener("click", () => {
                    showItem();
                    dumpJSON();
                    Oracle.change_item();
                });
        });

        function showItem() {
            document.querySelector(".itemButton")
                .innerHTML = Oracle.current_item();
        }

        function dumpJSON() {
            document.querySelector(".jsonDump")
                .value = Oracle.dump_json();
        }
    </script>
</body>
</html>

Running this wasm-packwp command generates the pkg directory and files used in the index.html file.

wasm-pack build --target web

Simple(ish) But Effective

Here's what it looks like on a page:

Bits and Bobs

A few notes on how things fit together:

  • The wasm_json_data_parser.js file pulls in a second wasm_json_data_parser_bg.wasm file. They are 9KB and 108KB, respectively. While that's a sizeable payload for such basic functionality, it's not unreasonable. Especially if it gets the thing I'm working on to actually work.
  • This is my first run at both wasm and keeping persistent data around inside it. It feels like there's an extra step in the middle where the JavaScript hits Oracle which then calls out to the Knowledge instance for the actual functionality. I took a few runs are removing that layer. Haven't hit on anything that works yet.
  • It also feels like there's an extra layer holding Data inside Knowledge. I couldn't figure out how to load the JSON directly into Knowledge. It's created before the JSON comes with:

    static GLOBAL_KNOWLEDGE: Lazy<Mutex<Knowledge>> =
      Lazy::new(|| Mutex::new(Knowledge::new()));
  • It feels like there should be a way to reduce the duplication of the lines unlocking GLOBAL_KNOWLEDGE. I tried moving the repeated lines to a single funciton and calling it. Didn't figure that out yet.
  • Calling everything as functions on the class feels weird. That is, doing this:

    Oracle.current_item();
    Oracle.change_item();
    Oracle.dump_json();

    Instead of something like:

    const oracle = new Oracle();
    oracle.current_item();
    oracle.change_item();
    oracle.dump_json();

    I tried to set that up. All I got was errors.

    But, whatever, calling on the class works just fine.

Onward

Overall, I very happy with the code. Sure, it would be great to figure how to slim things down. But, I'll take working code any way I can get it. Especially when it's my first crack at it.

Next step is to use this structure to implement the features for me Deck Refiner. I'll write up the results of that when I'm done.

-a

end of line

References

This is the main thing in the mix for doing the wasm.

The bog standard way to work with data structures.

Footnotes

The idea is to throw a bunch of possible cards into an oversized deck then whittle it down to 100 for a commander deck.

The preferred way to check how long things are taking to do their thing on a web page.

A quick run getting a WebAssembly module up and running in Rust.

Very cool tool for searching for cards and assembling decks. My thing is based off it. Just with another way to look at cards to refine decks.

The Archidekt API

Archidekt let's you pull data from their API. For example, here the JSON for one of my earlier decks:

https://archidekt.com/api/decks/19207437/

Create, Read, Update, and Delete. The most fundamental of tooling interactions.

I'm still getting my head wrapped around wasm, but the general idea is to use something like wasm-pack to compile your rust into a javascript file and a wasm file. You call the JS from your page which then does incantations to pull in the wasm.

You'll need to install wasm-pack to run the command.