Vibe Engines
YouTube

Lesson 8.1 · 9 min

Messages become JSON

A real model does not understand a Rust struct. It understands JSON, a plain-text format that every language can read. This lesson teaches Chatterbox to turn its Message into JSON and back, using a crate called serde.

Adds to ChatterboxMessages become JSON.

Why JSON

Everything Chatterbox has done so far stayed inside one program. A model lives somewhere else, on another computer, so the two have to agree on how to write a message down as text that travels over a network. That text is almost always JSON: curly braces for a record, square brackets for a list, and quoted names for fields.

Our Message has a role (who is talking) and some content (what they said). As JSON it looks like {"role":"user","content":"hi"}. Writing a struct down as text is called serializing. Reading text back into a struct is deserializing.

Crates: code other people wrote

Rust's standard library does not know JSON. A crate is a package of Rust code someone else wrote, and you ask for it by name in Cargo.toml, the file you met in lesson 1.2. Two crates do the work here: serde knows how to turn Rust values into and out of formats, and serde_json teaches it the JSON format.

From inside your project folder, cargo add serde --features derive and cargo add serde_json add both lines to Cargo.toml for you. The derive feature is the one that lets you write one line above a struct and get all the parsing code written for you.

One line above the struct

Here is a Message with two new words on it: Serialize and Deserialize. Those words in #[derive(...)] ask serde to write the conversion code. You do not write any of it.

One change from chapter 5, where the fields were author and text: for the API we rename them to role and content, because those are the names a model's API expects. The field names become the JSON keys, so they have to match.

src/main.rs: Chatterbox v0.8, Message knows JSON
use serde::{Deserialize, Serialize};
 
#[derive(Serialize, Deserialize, Debug)]
struct Message {
role: String,
content: String,
}
 
fn main() {
let m = Message { role: "user".to_string(), content: "hi".to_string() };
let text = serde_json::to_string(&m).unwrap();
println!("{text}");
 
let back: Message = serde_json::from_str(&text).unwrap();
println!("{back:?}");
}
$ cargo run
{"role":"user","content":"hi"}
Message { role: "user", content: "hi" }
PredictWhat does the JSON look like?
use serde::Serialize;
 
#[derive(Serialize)]
struct Message {
role: String,
content: String,
}
 
fn main() {
let m = Message { role: "assistant".to_string(), content: "Hello!".to_string() };
println!("{}", serde_json::to_string(&m).unwrap());
}

Which text does this program print?

  • {"role":"assistant","content":"Hello!"}
  • {"content":"Hello!","role":"assistant"}
  • Message { role: "assistant", content: "Hello!" }
Reveal the answer

The program prints: {"role":"assistant","content":"Hello!"} serde writes the fields in the order the struct declares them, with the field names as quoted keys. It is JSON text, not the debug form of the struct.

Reading can fail

Writing a Message as JSON always works: the struct is already shaped correctly. Reading is different, because the text came from outside. It might be missing a field, or not be JSON at all. That is why from_str returns a Result, the type from chapter 7. In the program above we used unwrap() to keep things short. In a real program you match on it, or use ?, so a bad reply becomes an error message instead of a crash.

This is the boundary where Rust earns its keep. If a model returns {"role":"assistant"} with no content, you find out right there, at the edge. You never carry a half-filled Message through the rest of the program.

Try itYour turn: write it down, read it back

Fill in to_json so it returns the JSON text for a Message, and from_json so it returns Some(message) for good JSON and None for text that does not fit. .ok() on a Result gives an Option: Ok(v) becomes Some(v) and Err(_) becomes None.

Fill in to_json so it returns the JSON text for a Message, and from_json so it returns Some(message) for good JSON and None for text that does not fit. .ok() on a Result gives an Option: Ok(v) becomes Some(v) and Err(_) becomes None.

use serde::{Deserialize, Serialize};
 
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Message {
role: String,
content: String,
}
 
fn to_json(m: &Message) -> String {
// return the JSON text for m
String::new()
}
 
fn from_json(text: &str) -> Option<Message> {
// Some(message) if the text fits, None if it does not
None
}
 
fn main() {
let m = Message { role: "user".to_string(), content: "hi".to_string() };
let text = to_json(&m);
println!("{text}");
println!("{:?}", from_json(&text));
}

Hidden tests (4): not_json_is_none writes_expected_json round_trip_gives_same_message missing_field_is_none

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

What you have now

Chatterbox can write a Message as JSON and read one back. That is half of talking to a model: the request goes out as JSON and the reply comes back as JSON. Next lesson, we put a name on "the thing that produces replies", so the same chat loop can use a fake one or a real one.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What are serializing and deserializing?Reveal
Serializing writes a Rust value as text (here JSON). Deserializing reads text back into a Rust value.
Why does <code>from_str</code> return a Result when <code>to_string</code> practically never fails?Reveal
The text comes from outside the program, so it can be missing fields or not be JSON at all. The struct you write out is already shaped correctly.
What does <code>#[derive(Serialize, Deserialize)]</code> do?Reveal
It asks serde to write the conversion code for your struct at compile time, so you do not write it by hand.

That is lesson 8.1. Tick it off so the course remembers where you are.

Next: 8.2 Traits →

Questions people ask

What is JSON?

A plain-text format for data: curly braces for records, square brackets for lists, and quoted field names. Nearly every web API, including model APIs, uses it.

What is a crate in Rust?

A package of Rust code. You list the crates your project needs in Cargo.toml and cargo downloads and builds them.

Do I need serde to use JSON in Rust?

In practice yes. The standard library has no JSON support, and serde with serde_json is the common choice.

Learn Rust by building a chat app · Lesson 8.1 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026