Skip to content

JSON to Rust Struct (Serde)

Convert JSON to Rust structs with serde derives, ready for serde_json::from_str.

Input · JSON
Output · Rust
Valid JSONRuns in your browser

Load from a URL

Your browser fetches it directly, so the server must allow cross-origin requests (CORS).

Crate metadata: JSON to Rust structs

With Crate as the Root name, the sample registry record becomes:

{
  "name": "serde_json",
  "max_version": "1.0.133",
  "downloads": 486211953,
  "categories": ["encoding", "parser-implementations"],
  "repository": "https://github.com/serde-rs/json",
  "links": {
    "owners": "/api/v1/crates/serde_json/owners",
    "versions": null
  },
  "yanked": false
}
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Crate {
    pub name: String,
    pub max_version: String,
    pub downloads: i64,
    pub categories: Vec<String>,
    pub repository: String,
    pub links: Links,
    pub yanked: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Links {
    pub owners: String,
    pub versions: Option<serde_json::Value>,
}

Fields are pub, whole numbers i64, decimals f64 and arrays Vec<T>. With only a null to go on, versions is Option<serde_json::Value>; until you set the real type, inspect it with methods such as .as_str() or .as_array(). downloads can’t be negative, so u64 is a fair edit.

Parse it

Add serde with its derive feature, and serde_json, to Cargo.toml:

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let body = std::fs::read_to_string("crate.json")?;
    let krate: Crate = serde_json::from_str(&body)?;
    println!("{} has {} downloads", krate.name, krate.downloads);
    Ok(())
}

crate is a keyword, hence krate. serde_json::to_string_pretty(&krate)? writes it back out. With reqwest, enable its json feature and call .json::<Crate>() on the response. Serde ignores undeclared keys unless the struct has #[serde(deny_unknown_fields)]. If from_str fails with expected value at line 1 column 1, the input isn’t JSON, often an HTML error page; check it in the JSON validator.

Renames, raw identifiers and Option

Fields are snake_case; where that changes a key, #[serde(rename = "...")] keeps the original, and a key that is a Rust keyword becomes a raw identifier. With Events as the root name:

[
  { "id": 1, "type": "push", "createdAt": "2026-11-01T10:00:00Z" },
  { "id": 2, "type": "release", "createdAt": "2026-11-03T08:30:00Z", "tag": "v1.4.0" }
]
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
    pub id: i64,
    pub r#type: String,
    #[serde(rename = "createdAt")]
    pub created_at: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tag: Option<String>,
}

tag is missing from the first event, so it is Option<String> with #[serde(default, skip_serializing_if = "Option::is_none")]: decoding accepts the missing key and encoding omits it again. A key that was present but null, like versions above, is an Option without the skip, so it serializes back to null. Both come from merging array items first, as JSON to TypeScript explains; JSON to Go handles the same cases with pointers.

If every key in a struct follows one convention, you can replace the per-field renames with a single #[serde(rename_all = "camelCase")] on the struct.

Questions

Why does a field start with r#?

Its JSON key is a Rust keyword, such as type or ref. r#type is a raw identifier, the Rust syntax for using a keyword as a name, and serde still reads and writes the key as plain type.

Why derive Debug and Clone?

Debug lets you print a value with {:?} and Clone lets you copy it. Serde needs neither, so remove them if you prefer.

Do the structs work with formats other than JSON?

Yes. The derives and attributes belong to serde, not serde_json, so the same structs load from TOML or YAML with the matching crate.

Related tools

Esc