Rust Serde JSON
last modified November 13, 2026
In this article we work with JSON data in Rust using the serde and serde_json crates.
JSON (JavaScript Object Notation) is a lightweight text format
for exchanging data. It has objects, arrays, strings, numbers, booleans,
and null. The Rust standard library has no JSON support; the
de facto standard is Serde, a framework for
serializing and deserializing Rust data structures.
Serde itself is format-agnostic. The serde crate defines the
Serialize and Deserialize traits and derive
macros that implement them; format crates such as
serde_json, toml, or serde_yaml
read and write the actual text. The examples were tested with Rust 1.97,
the 2024 edition, serde 1.0.229, and serde_json 1.0.154. They are run as
Cargo projects with cargo run -q.
Setting up the project
Both crates are added to the project's dependencies. The
derive feature of serde enables the
#[derive(Serialize, Deserialize)] macros.
[package]
name = "json_example"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.154"
The serde_json crate brings serde in on its own,
but the derive macros are only available when we depend on
serde directly with the feature enabled. The dependencies
are added with cargo add, which also lists the available
features of each crate.
$ cargo add serde --features derive
Updating crates.io index
Adding serde v1.0.229 to dependencies
Features:
+ derive
+ serde_derive
+ std
- alloc
- rc
- unstable
Updating crates.io index
Locking 7 packages to latest Rust 1.97.1 compatible versions
$ cargo add serde_json
Updating crates.io index
Adding serde_json v1.0.154 to dependencies
Features:
+ std
- alloc
- arbitrary_precision
- float_roundtrip
- indexmap
- preserve_order
- raw_value
- unbounded_depth
Updating crates.io index
Locking 4 packages to latest Rust 1.97.1 compatible versions
Adding itoa v1.0.19
Adding memchr v2.8.3
Adding serde_json v1.0.154
Adding zmij v1.0.23
Serializing a struct
Deriving Serialize lets a struct be converted to JSON.
use serde::Serialize;
#[derive(Serialize)]
struct User {
name: String,
age: u32,
email: String,
active: bool,
roles: Vec<String>,
}
fn main() -> Result<(), serde_json::Error> {
let user = User {
name: "John Doe".to_string(),
age: 34,
email: "john@example.com".to_string(),
active: true,
roles: vec!["admin".to_string(), "editor".to_string()],
};
let compact = serde_json::to_string(&user)?;
println!("{compact}");
let pretty = serde_json::to_string_pretty(&user)?;
println!("{pretty}");
Ok(())
}
The example turns a user record into a JSON string.
#[derive(Serialize)]
struct User {
The derive macro generates the Serialize implementation.
Every field type must be serializable too; all standard types such as
numbers, strings, vectors, and options are.
let compact = serde_json::to_string(&user)?;
The to_string function produces compact JSON on a single
line. Struct fields become object keys, in declaration order.
let pretty = serde_json::to_string_pretty(&user)?;
The to_string_pretty function indents the output with two
spaces. Serialization of a struct like this cannot actually fail, but
the functions return Result because some types can, for
instance a map with non-string keys.
$ cargo run -q
{"name":"John Doe","age":34,"email":"john@example.com","active":true,"roles":["admin","editor"]}
{
"name": "John Doe",
"age": 34,
"email": "john@example.com",
"active": true,
"roles": [
"admin",
"editor"
]
}
Deserializing into a struct
Deriving Deserialize lets a struct be built from JSON.
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct User {
name: String,
age: u32,
roles: Vec<String>,
}
fn main() -> Result<(), serde_json::Error> {
let data = r#"
{
"name": "Roger Roe",
"age": 41,
"roles": ["viewer"],
"country": "SK"
}"#;
let user: User = serde_json::from_str(data)?;
println!("{user:?}");
println!("{} is {} years old, roles: {:?}", user.name, user.age, user.roles);
let ages: Vec<u32> = serde_json::from_str("[31, 45, 27]")?;
let total: u32 = ages.iter().sum();
println!("total age: {total}");
Ok(())
}
The JSON text is parsed into a typed struct.
let user: User = serde_json::from_str(data)?;
The from_str function needs to know the target type, given
here by the type annotation. Whitespace in the input does not matter.
"country": "SK"
The JSON has a country key that the struct does not have.
By default, Serde silently ignores unknown fields.
let ages: Vec<u32> = serde_json::from_str("[31, 45, 27]")?;
Any deserializable type can be the target, not only structs. A JSON array becomes a vector.
$ cargo run -q
User { name: "Roger Roe", age: 41, roles: ["viewer"] }
Roger Roe is 41 years old, roles: ["viewer"]
total age: 103
A round trip
Types that derive both traits can go to JSON and back without losing information.
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Point {
x: f64,
y: f64,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Polygon {
name: String,
points: Vec<Point>,
closed: bool,
}
fn main() -> Result<(), serde_json::Error> {
let poly = Polygon {
name: "triangle".into(),
points: vec![Point { x: 0.0, y: 0.0 }, Point { x: 4.0, y: 0.0 }, Point { x: 0.0, y: 3.0 }],
closed: true,
};
let json = serde_json::to_string(&poly)?;
println!("{json}");
let back: Polygon = serde_json::from_str(&json)?;
println!("{back:?}");
println!("equal: {}", poly == back);
Ok(())
}
A polygon contains a vector of nested point structs.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Polygon {
name: String,
points: Vec<Point>,
closed: bool,
}
Nested structs become nested JSON objects, and vectors become arrays.
Floats are always written with a decimal point, so 0.0
stays a float.
println!("equal: {}", poly == back);
The value read back is equal to the original.
$ cargo run -q
{"name":"triangle","points":[{"x":0.0,"y":0.0},{"x":4.0,"y":0.0},{"x":0.0,"y":3.0}],"closed":true}
Polygon { name: "triangle", points: [Point { x: 0.0, y: 0.0 }, Point { x: 4.0, y: 0.0 }, Point { x: 0.0, y: 3.0 }], closed: true }
equal: true
Untyped JSON with Value
When the structure of the data is not known in advance, JSON can be
parsed into serde_json::Value, an enum that represents any
JSON value.
use serde_json::{Value, json};
fn main() -> Result<(), serde_json::Error> {
let data = r#"{
"city": "Bratislava",
"population": 475000,
"coords": {"lat": 48.1486, "lon": 17.1077},
"districts": ["Old Town", "Petrzalka", "Ruzinov"]
}"#;
let v: Value = serde_json::from_str(data)?;
println!("{}", v["city"]);
println!("{:?}", v["city"].as_str());
println!("{}", v["coords"]["lat"]);
println!("{}", v["districts"][1]);
println!("{}", v["districts"].as_array().map_or(0, |a| a.len()));
println!("{}", v["mayor"]);
println!("{:?}", v["population"].as_u64());
println!("{:?}", v["population"].as_str());
let name = "Anna";
let tags = vec!["rust", "json"];
let doc = json!({
"user": name,
"score": 9.5,
"tags": tags,
"address": null,
"verified": true
});
println!("{doc}");
Ok(())
}
The Value enum has the variants Null,
Bool, Number, String,
Array, and Object.
println!("{}", v["city"]);
println!("{:?}", v["city"].as_str());
Indexing with a string looks up an object key, and indexing with a number
looks up an array element. Printing a Value outputs JSON,
so a string is shown with quotes. The as_str method extracts
the Rust string.
println!("{}", v["mayor"]);
Indexing never panics on missing data; a missing key yields
Null. The get method returns an
Option instead when the difference matters.
println!("{:?}", v["population"].as_str());
The as_* methods return None when the value has
a different type.
let doc = json!({
"user": name,
"score": 9.5,
"tags": tags,
...
});
The json! macro builds a Value with JSON
syntax. Rust variables and expressions can be interpolated. By default,
objects are stored in a BTreeMap, so the keys are printed
in sorted order; the preserve_order feature keeps the
insertion order.
$ cargo run -q
"Bratislava"
Some("Bratislava")
48.1486
"Petrzalka"
3
null
Some(475000)
None
{"address":null,"score":9.5,"tags":["rust","json"],"user":"Anna","verified":true}
Modifying a Value
A Value can be changed in place.
use serde_json::{Value, json};
fn main() -> Result<(), serde_json::Error> {
let mut v: Value = serde_json::from_str(
r#"{"order": 1001, "items": [{"sku": "A1", "qty": 2}, {"sku": "B7", "qty": 1}]}"#,
)?;
v["status"] = json!("shipped");
v["items"][0]["qty"] = json!(3);
if let Some(items) = v["items"].as_array_mut() {
items.push(json!({"sku": "C3", "qty": 5}));
}
if let Some(obj) = v.as_object_mut() {
obj.remove("order");
}
println!("{}", v.pointer("/items/2/sku").unwrap_or(&Value::Null));
let total: u64 = v["items"]
.as_array()
.into_iter()
.flatten()
.filter_map(|item| item["qty"].as_u64())
.sum();
println!("total qty: {total}");
println!("{}", serde_json::to_string_pretty(&v)?);
Ok(())
}
We update an order document.
v["status"] = json!("shipped");
v["items"][0]["qty"] = json!(3);
Assigning through a string index inserts or replaces an object key. Note that indexing a missing array element in assignment panics, unlike reading.
if let Some(items) = v["items"].as_array_mut() {
items.push(json!({"sku": "C3", "qty": 5}));
}
The as_array_mut and as_object_mut methods give
mutable access to the underlying Vec and map.
println!("{}", v.pointer("/items/2/sku").unwrap_or(&Value::Null));
The pointer method looks up a value by a JSON Pointer path
(RFC 6901), with keys and indexes separated by slashes.
$ cargo run -q
"C3"
total qty: 9
{
"items": [
{
"qty": 3,
"sku": "A1"
},
{
"qty": 1,
"sku": "B7"
},
{
"qty": 5,
"sku": "C3"
}
],
"status": "shipped"
}
Collections, tuples, and options
Standard Rust types map to JSON in a predictable way.
use std::collections::{BTreeMap, HashMap};
fn main() -> Result<(), serde_json::Error> {
let mut prices = BTreeMap::new();
prices.insert("pear", 1.2);
prices.insert("apple", 0.8);
prices.insert("plum", 2.5);
println!("{}", serde_json::to_string(&prices)?);
let mut by_id = BTreeMap::new();
by_id.insert(3, "Lucia");
by_id.insert(1, "Anna");
println!("{}", serde_json::to_string(&by_id)?);
let tuple = ("point", 3, -4);
println!("{}", serde_json::to_string(&tuple)?);
let nested: Vec<Option<i32>> = vec![Some(1), None, Some(3)];
println!("{}", serde_json::to_string(&nested)?);
let parsed: HashMap<String, Vec<u32>> = serde_json::from_str(r#"{"odd": [1, 3], "even": [2, 4]}"#)?;
println!("{:?}", parsed["even"]);
let back: BTreeMap<u32, String> = serde_json::from_str(r#"{"10": "ten", "2": "two"}"#)?;
println!("{back:?}");
Ok(())
}
We serialize maps, a tuple, and options, and parse two maps.
let mut by_id = BTreeMap::new(); by_id.insert(3, "Lucia");
JSON object keys are always strings. Integer keys are converted to strings when serializing and parsed back when deserializing.
let tuple = ("point", 3, -4);
Tuples become arrays.
let nested: Vec<Option<i32>> = vec![Some(1), None, Some(3)];
Some(x) is serialized as the plain value and
None as null.
$ cargo run -q
{"apple":0.8,"pear":1.2,"plum":2.5}
{"1":"Anna","3":"Lucia"}
["point",3,-4]
[1,null,3]
[2, 4]
{2: "two", 10: "ten"}
Optional and default fields
Field attributes control how missing and empty values are handled.
use serde::{Deserialize, Serialize};
fn default_lang() -> String {
"en".to_string()
}
#[derive(Debug, Serialize, Deserialize)]
struct Profile {
username: String,
#[serde(skip_serializing_if = "Option::is_none")]
nickname: Option<String>,
#[serde(default)]
followers: u32,
#[serde(default = "default_lang")]
language: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
tags: Vec<String>,
#[serde(skip)]
session_token: String,
}
fn main() -> Result<(), serde_json::Error> {
let p: Profile = serde_json::from_str(r#"{"username": "jdoe", "nickname": null}"#)?;
println!("{p:?}");
println!("{}", serde_json::to_string(&p)?);
let full = r#"{"username": "anna", "nickname": "Annie", "followers": 120,
"language": "sk", "tags": ["rust"], "session_token": "secret"}"#;
let p: Profile = serde_json::from_str(full)?;
println!("{p:?}");
println!("{}", serde_json::to_string(&p)?);
println!("token read: {}", !p.session_token.is_empty());
Ok(())
}
A user profile has several optional parts.
#[serde(skip_serializing_if = "Option::is_none")] nickname: Option<String>,
An Option field accepts a missing key or null
when deserializing. The attribute omits the key from the output when the
value is None, instead of writing null.
#[serde(default)] followers: u32, #[serde(default = "default_lang")] language: String,
Without these attributes, a missing followers key would be
an error. The default attribute uses
Default::default(); the second form calls the named
function.
#[serde(skip)] session_token: String,
A skipped field is never written or read; it is always set to its default value. The token in the input JSON was ignored.
$ cargo run -q
Profile { username: "jdoe", nickname: None, followers: 0, language: "en", tags: [], session_token: "" }
{"username":"jdoe","followers":0,"language":"en"}
Profile { username: "anna", nickname: Some("Annie"), followers: 120, language: "sk", tags: ["rust"], session_token: "" }
{"username":"anna","nickname":"Annie","followers":120,"language":"sk","tags":["rust"]}
token read: false
Renaming fields
JSON APIs often use naming conventions that differ from Rust's snake_case.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Book {
title: String,
author_name: String,
page_count: u32,
#[serde(rename = "ISBN")]
isbn: String,
#[serde(alias = "published", alias = "year")]
published_year: u16,
}
fn main() -> Result<(), serde_json::Error> {
let data = r#"{
"title": "The Book of Rust",
"authorName": "Jane Doe",
"pageCount": 512,
"ISBN": "978-0-00-000000-0",
"year": 2026
}"#;
let book: Book = serde_json::from_str(data)?;
println!("{book:?}");
println!("{}", serde_json::to_string_pretty(&book)?);
Ok(())
}
The struct maps between Rust names and camelCase JSON keys.
#[serde(rename_all = "camelCase")]
struct Book {
The container attribute renames all fields; author_name
becomes authorName. Other options include
"kebab-case", "PascalCase", and
"SCREAMING_SNAKE_CASE".
#[serde(rename = "ISBN")] isbn: String, #[serde(alias = "published", alias = "year")] published_year: u16,
The rename attribute sets the name of one field. An
alias is an additional name accepted when deserializing;
the field is still written under its main name.
$ cargo run -q
Book { title: "The Book of Rust", author_name: "Jane Doe", page_count: 512, isbn: "978-0-00-000000-0", published_year: 2026 }
{
"title": "The Book of Rust",
"authorName": "Jane Doe",
"pageCount": 512,
"ISBN": "978-0-00-000000-0",
"publishedYear": 2026
}
Enum representations
JSON has no enums, so Serde offers several ways to represent them.
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
enum Shape {
Circle { radius: f64 },
Rect { w: f64, h: f64 },
Unit,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
enum Event {
Login { user: String },
Purchase { user: String, amount: f64 },
Logout { user: String },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum Level {
Info,
Warning,
Error,
}
fn main() -> Result<(), serde_json::Error> {
let shapes = vec![Shape::Circle { radius: 1.5 }, Shape::Rect { w: 2.0, h: 3.0 }, Shape::Unit];
println!("{}", serde_json::to_string(&shapes)?);
let events: Vec<Event> = serde_json::from_str(
r#"[{"type": "login", "user": "anna"},
{"type": "purchase", "user": "anna", "amount": 19.9},
{"type": "logout", "user": "anna"}]"#,
)?;
for e in &events {
match e {
Event::Purchase { user, amount } => println!("{user} spent {amount}"),
other => println!("{other:?}"),
}
}
println!("{}", serde_json::to_string(&events[1])?);
let levels: Vec<Level> = serde_json::from_str(r#"["INFO", "ERROR"]"#)?;
println!("{levels:?} {}", serde_json::to_string(&Level::Warning)?);
Ok(())
}
We use the default representation, an internally tagged one, and unit-only variants.
enum Shape {
Circle { radius: f64 },
Rect { w: f64, h: f64 },
Unit,
}
By default, enums are externally tagged: a variant with data becomes an object with a single key, the variant name. A unit variant is just its name as a string.
#[serde(tag = "type", rename_all = "lowercase")]
enum Event {
The internally tagged form puts the variant name in a field next to the data. This is the common format of event streams and web APIs.
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum Level {
On an enum, rename_all renames the variants. Enums with
only unit variants map nicely to JSON string constants.
$ cargo run -q
[{"Circle":{"radius":1.5}},{"Rect":{"w":2.0,"h":3.0}},"Unit"]
Login { user: "anna" }
anna spent 19.9
Logout { user: "anna" }
{"type":"purchase","user":"anna","amount":19.9}
[Info, Error] "WARNING"
Untagged enums
An untagged enum has no marker in the data; Serde tries each variant in order and picks the first that matches.
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Id {
Number(u64),
Text(String),
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Tags {
One(String),
Many(Vec<String>),
}
#[derive(Debug, Deserialize)]
struct Record {
id: Id,
tags: Tags,
}
fn main() -> Result<(), serde_json::Error> {
let data = r#"[
{"id": 17, "tags": "new"},
{"id": "A-200", "tags": ["sale", "summer"]}
]"#;
let records: Vec<Record> = serde_json::from_str(data)?;
for r in &records {
println!("{r:?}");
let id = match &r.id {
Id::Number(n) => format!("#{n}"),
Id::Text(s) => s.clone(),
};
let tags = match &r.tags {
Tags::One(t) => vec![t.as_str()],
Tags::Many(v) => v.iter().map(String::as_str).collect(),
};
println!("{id}: {}", tags.join(", "));
}
let bad = serde_json::from_str::<Record>(r#"{"id": true, "tags": "x"}"#);
println!("{}", bad.unwrap_err());
Ok(())
}
The data uses either a number or a string as an identifier, and either a single tag or a list of tags.
#[serde(untagged)]
enum Tags {
One(String),
Many(Vec<String>),
}
A string matches the One variant, an array the
Many variant. Untagged enums are convenient for loosely
specified formats.
let bad = serde_json::from_str::<Record>(r#"{"id": true, "tags": "x"}"#);
The drawback is the error message: when no variant fits, Serde can only say that nothing matched, not why.
$ cargo run -q
Record { id: Number(17), tags: One("new") }
#17: new
Record { id: Text("A-200"), tags: Many(["sale", "summer"]) }
A-200: sale, summer
data did not match any variant of untagged enum Id at line 1 column 11
Handling errors
Deserialization fails on invalid JSON and on JSON that does not fit the
target type. The serde_json::Error type reports what went
wrong and where.
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Config {
host: String,
port: u16,
}
fn main() {
let inputs = [
r#"{"host": "localhost", "port": 8080}"#,
r#"{"host": "localhost"}"#,
r#"{"host": "localhost", "port": 70000}"#,
r#"{"host": "localhost", "port": "80"}"#,
r#"{"host": "localhost", "port": 80, "debug": true}"#,
"{\"host\": \"localhost\",\n \"port\": 80,\n}",
r#"{"host": "localhost", "port": 80"#,
];
for input in inputs {
match serde_json::from_str::<Config>(input) {
Ok(cfg) => println!("ok: {}:{}", cfg.host, cfg.port),
Err(e) => println!("{:?} error: {e}", e.classify()),
}
}
}
We parse several broken configurations.
#[serde(deny_unknown_fields)]
struct Config {
This attribute turns unknown keys into an error, which catches typos in configuration files.
Err(e) => println!("{:?} error: {e}", e.classify()),
The classify method returns the error category:
Syntax for malformed JSON, Data for valid JSON
with the wrong shape, Eof for truncated input, and
Io for read errors. The message includes the line and
column. Note that 70000 does not fit into a
u16, and the string "80" is not accepted as a
number. JSON does not allow trailing commas.
$ cargo run -q ok: localhost:8080 Data error: missing field `port` at line 1 column 21 Data error: invalid value: integer `70000`, expected u16 at line 1 column 35 Data error: invalid type: string "80", expected u16 at line 1 column 34 Data error: unknown field `debug`, expected `host` or `port` at line 1 column 41 Syntax error: trailing comma at line 3 column 1 Eof error: EOF while parsing an object at line 1 column 32
Writing JSON to a file
The to_writer and to_writer_pretty functions
write JSON to anything that implements io::Write.
use serde::Serialize;
use std::fs::File;
use std::io::BufWriter;
#[derive(Serialize)]
struct User {
id: u32,
name: String,
email: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let users = vec![
User { id: 1, name: "Anna".into(), email: "anna@example.com".into() },
User { id: 2, name: "Peter".into(), email: "peter@example.com".into() },
User { id: 3, name: "Lucia".into(), email: "lucia@example.com".into() },
];
let file = File::create("users.json")?;
let mut writer = BufWriter::new(file);
serde_json::to_writer_pretty(&mut writer, &users)?;
println!("wrote {} users", users.len());
Ok(())
}
We write a list of users to a file.
let file = File::create("users.json")?;
let mut writer = BufWriter::new(file);
serde_json::to_writer_pretty(&mut writer, &users)?;
Serde writes many small pieces, so the file is wrapped in a
BufWriter. The main function returns
Box<dyn Error> because two error types are involved:
io::Error and serde_json::Error. The buffer is
flushed when the writer is dropped; a call to flush would
report write errors that dropping silently ignores.
$ cargo run -q
wrote 3 users
$ cat users.json
[
{
"id": 1,
"name": "Anna",
"email": "anna@example.com"
},
{
"id": 2,
"name": "Peter",
"email": "peter@example.com"
},
{
"id": 3,
"name": "Lucia",
"email": "lucia@example.com"
}
]
Reading JSON from a file
The from_reader function reads JSON from anything that
implements io::Read.
use serde::Deserialize;
use std::fs::File;
use std::io::BufReader;
#[derive(Debug, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::open("users.json")?;
let reader = BufReader::new(file);
let users: Vec<User> = serde_json::from_reader(reader)?;
for u in &users {
println!("{:>2} {:<6} {}", u.id, u.name, u.email);
}
let text = std::fs::read_to_string("users.json")?;
let users2: Vec<User> = serde_json::from_str(&text)?;
println!("{} users, last: {:?}", users2.len(), users2.last());
Ok(())
}
We read the users written by the previous example.
let reader = BufReader::new(file); let users: Vec<User> = serde_json::from_reader(reader)?;
Reading from an unbuffered file would be slow, so we wrap it in a
BufReader.
let text = std::fs::read_to_string("users.json")?;
let users2: Vec<User> = serde_json::from_str(&text)?;
Reading the whole file into a string and calling from_str
is usually faster than from_reader, at the cost of holding
the text in memory.
$ cargo run -q
1 Anna anna@example.com
2 Peter peter@example.com
3 Lucia lucia@example.com
3 users, last: Some(User { id: 3, name: "Lucia", email: "lucia@example.com" })
Zero-copy deserialization
A struct can borrow string data directly from the input instead of allocating new strings.
use serde::Deserialize;
use std::borrow::Cow;
#[derive(Debug, Deserialize)]
struct LogLine<'a> {
level: &'a str,
#[serde(borrow)]
message: Cow<'a, str>,
}
fn main() -> Result<(), serde_json::Error> {
let input = r#"[
{"level": "info", "message": "server started"},
{"level": "warn", "message": "disk \"/data\" almost full"}
]"#;
let lines: Vec<LogLine> = serde_json::from_str(input)?;
for line in &lines {
let kind = match line.message {
Cow::Borrowed(_) => "borrowed",
Cow::Owned(_) => "owned",
};
println!("[{}] {} ({kind})", line.level, line.message);
}
let bad: Result<LogLine, _> = serde_json::from_str(r#"{"level": "in\"fo", "message": "x"}"#);
println!("{}", bad.unwrap_err());
Ok(())
}
The struct has a lifetime parameter tied to the input text.
struct LogLine<'a> {
level: &'a str,
#[serde(borrow)]
message: Cow<'a, str>,
}
A &str field points into the JSON text. This only works
when the string in the input has no escape sequences, because an escaped
string must be decoded into new memory. A Cow<str>
field borrows when it can and allocates when it must; the
borrow attribute tells Serde to try borrowing.
serde_json::from_str(r#"{"level": "in\"fo", "message": "x"}"#);
The level contains an escaped quote, so it cannot be borrowed, and the
&str field fails.
$ cargo run -q [info] server started (borrowed) [warn] disk "/data" almost full (owned) invalid type: string "in\"fo", expected a borrowed string at line 1 column 18
Flattening
The flatten attribute merges the fields of a nested struct
or map into the parent object.
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug, Serialize, Deserialize)]
struct Pagination {
page: u32,
per_page: u32,
}
#[derive(Debug, Serialize, Deserialize)]
struct Request {
query: String,
#[serde(flatten)]
pagination: Pagination,
#[serde(flatten)]
extra: BTreeMap<String, Value>,
}
fn main() -> Result<(), serde_json::Error> {
let data = r#"{"query": "rust", "page": 2, "per_page": 20, "sort": "date", "safe": true}"#;
let req: Request = serde_json::from_str(data)?;
println!("{req:?}");
println!("page {} of results for {:?}", req.pagination.page, req.query);
println!("extra keys: {:?}", req.extra.keys().collect::<Vec<_>>());
println!("{}", serde_json::to_string(&req)?);
Ok(())
}
A search request mixes known and unknown parameters at one level.
#[serde(flatten)] pagination: Pagination,
The page and per_page keys are read into the
nested struct, although they are not nested in the JSON. This lets us
reuse groups of fields in several types.
#[serde(flatten)] extra: BTreeMap<String, Value>,
A flattened map collects all remaining keys. This preserves unknown data when a document is read and written back.
$ cargo run -q
Request { query: "rust", pagination: Pagination { page: 2, per_page: 20 }, extra: {"safe": Bool(true), "sort": String("date")} }
page 2 of results for "rust"
extra keys: ["safe", "sort"]
{"query":"rust","page":2,"per_page":20,"safe":true,"sort":"date"}
Streaming JSON values
Log files and data exports often use newline-delimited JSON (NDJSON), with
one JSON value per line. A StreamDeserializer reads such
input value by value.
use serde::Deserialize;
use serde_json::Deserializer;
#[derive(Debug, Deserialize)]
struct Reading {
sensor: String,
temp: f64,
}
fn main() {
let input = r#"{"sensor": "a", "temp": 21.5}
{"sensor": "b", "temp": 19.0}
{"sensor": "a", "temp": 22.1}
{"sensor": "c", "temp": "hot"}
{"sensor": "b", "temp": 18.4}"#;
let stream = Deserializer::from_str(input).into_iter::<Reading>();
let mut sum = 0.0;
let mut count = 0;
for item in stream {
match item {
Ok(r) => {
println!("{} {}", r.sensor, r.temp);
sum += r.temp;
count += 1;
}
Err(e) => {
println!("stopped: {e}");
break;
}
}
}
println!("average of {count}: {:.2}", sum / count as f64);
}
We compute the average temperature of sensor readings.
let stream = Deserializer::from_str(input).into_iter::<Reading>();
The iterator yields a Result for each value. The values
can be separated by any whitespace, so the input does not even need
newlines. There is also Deserializer::from_reader for files
and sockets.
Err(e) => {
println!("stopped: {e}");
break;
}
After an error the stream cannot reliably continue, so we stop. To skip bad lines, read the input line by line and parse each line separately.
$ cargo run -q a 21.5 b 19 a 22.1 stopped: invalid type: string "hot", expected f64 at line 4 column 29 average of 3: 20.87
Numbers
JSON has a single number type, while Rust has many. serde_json keeps integers and floats apart and checks the ranges.
use serde_json::{Value, json};
fn main() -> Result<(), serde_json::Error> {
let v: Value = serde_json::from_str(r#"{"big": 18446744073709551615, "neg": -5, "pi": 3.14, "exp": 1e3}"#)?;
for key in ["big", "neg", "pi", "exp"] {
let n = &v[key];
println!("{key}: u64={:?} i64={:?} f64={:?}", n.as_u64(), n.as_i64(), n.as_f64());
}
println!("{}", json!(f64::NAN));
println!("{}", json!([1.0, 0.1 + 0.2, 1e21]));
let r: Result<u8, _> = serde_json::from_str("300");
println!("{}", r.unwrap_err());
let r: Result<i32, _> = serde_json::from_str("2.5");
println!("{}", r.unwrap_err());
Ok(())
}
We inspect numbers in a Value and parse into narrow types.
println!("{key}: u64={:?} i64={:?} f64={:?}", n.as_u64(), n.as_i64(), n.as_f64());
A number without a decimal point or exponent is stored as an integer, so
the largest u64 is read exactly. Converting it to
f64 loses precision. Numbers written with a decimal point or
an exponent, such as 1e3, are floats.
println!("{}", json!(f64::NAN));
JSON cannot represent NaN or infinity. serde_json converts them to
null.
let r: Result<u8, _> = serde_json::from_str("300");
A number that does not fit the target type is an error, not a silent
truncation as with the as operator.
$ cargo run -q big: u64=Some(18446744073709551615) i64=None f64=Some(1.8446744073709552e19) neg: u64=None i64=Some(-5) f64=Some(-5.0) pi: u64=None i64=None f64=Some(3.14) exp: u64=None i64=None f64=Some(1000.0) null [1.0,0.30000000000000004,1e+21] invalid value: integer `300`, expected u8 at line 1 column 3 invalid type: floating point `2.5`, expected i32 at line 1 column 3
Source
serde_json crate documentation
In this article we have worked with JSON in Rust using serde and serde_json. Reading and writing files is covered in Rust read file and Rust write file.
Author
List all Rust tutorials.