C.W.K.
Stream
Lesson 04 of 07 · published

serde: The Type Bridge

~14 min · tauri, serde, serialization, rust

Level 0Web Tourist
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"serde is the translator standing on the bridge, turning Rust structs into JSON and back without you writing a line of parsing."

Derive It and Forget It

Anything that crosses the IPC bridge must become plain data. In Rust, the crate that does this is serde. You annotate a struct or enum with #[derive(Serialize, Deserialize)] and serde generates the conversion code: Serialize lets it travel to the frontend (as a command's return), Deserialize lets it arrive from the frontend (as a command's argument). No manual JSON building, ever.

Structs Both Ways

A command can take a struct and return a struct. The frontend sends a plain object that deserializes into your Rust struct; the command returns a struct that serializes into a plain object the webview receives. This is how rich data crosses cleanly — you think in typed shapes on both ends, and serde handles the wire format in the middle.

Make the Field Names Match the Frontend

Rust structs use snake_case fields (created_at), but your TypeScript probably wants camelCase (createdAt). Add #[serde(rename_all = "camelCase")] to the struct and serde renames every field on the wire. Now your Rust stays idiomatic and your frontend receives the casing it expects — the same philosophy as the argument convention, applied to data shapes.

Code

A struct that crosses both ways·rust
use serde::{Serialize, Deserialize};

// rename_all makes the wire JSON camelCase for a happy TS frontend,
// while Rust keeps idiomatic snake_case fields.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Note {
    id: u32,
    title: String,
    created_at: String,   // arrives in JS as `createdAt`
}

// Takes a struct in, returns a struct out — serde handles both directions.
#[tauri::command]
fn save_note(draft: Note) -> Note {
    // ... persist it ...
    draft  // serialized back to the webview as a plain object
}
Frontend sees clean camelCase·tsx
// The matching TypeScript shape (camelCase, thanks to rename_all).
interface Note {
  id: number;
  title: string;
  createdAt: string;
}

const saved = await invoke<Note>("save_note", {
  draft: { id: 1, title: "Hi", createdAt: new Date().toISOString() },
});

External links

Exercise

Define a struct with at least one multi-word field (like created_at), derive Serialize/Deserialize, and add #[serde(rename_all = "camelCase")]. Write a command that returns it, invoke it from the frontend, and confirm in the console that the field arrives as camelCase. Then remove the rename_all line and watch the field name change — that's serde's renaming made visible.
Hint
Without rename_all, your TS will receive created_at; with it, createdAt. The matching TS interface should use camelCase. This is the cleanest way to keep both languages idiomatic.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.