Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the json to rust struct engine, best practices for implementation, and data security standards.
Rust's serde framework is the gold standard for serialization — not because of a runtime engine, but because it compiles your JSON mapping logic into specialized code at build time via derive macros. Every #[derive(Deserialize)] on your struct generates a custom deserializer that is as fast as hand-written code. Converting your JSON to Rust structs with the right serde attributes gives you correctness enforced by the borrow checker, performance with zero garbage collection pauses, and a rich attribute system that handles every JSON quirk without a single runtime reflection call.
// Input JSON
{
"id": 42,
"title": "Rust in Production",
"author_name": "Ferris Dev",
"is_published": true,
"tags": ["systems", "performance"],
"view_count": 1500,
"published_at": "2024-01-15T08:30:00Z"
}
// Generated Rust Struct
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Article {
pub id: u32,
pub title: String,
pub author_name: String,
pub is_published: bool,
pub tags: Vec<String>,
pub view_count: u64,
#[serde(with = "chrono::serde::ts_seconds_option")]
pub published_at: Option<DateTime<Utc>>,
}
// Deserialize from JSON string
let article: Article = serde_json::from_str(json_str)?;
// Serialize to JSON
let json = serde_json::to_string_pretty(&article)?;
#[serde(rename_all = "snake_case")] converts all field names to snake_case in the JSON representation — you write idiomatic Rust field names and serde handles the JSON naming convention automatically. For the reverse, "camelCase" converts to camelCase JSON keys.
Rust's Option<T> is the correct type for both missing fields and null values, but serde's behavior can be configured precisely:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct UserProfile {
pub id: u64,
pub email: String,
// Field may be absent from JSON — defaults to None
#[serde(default)]
pub bio: Option<String>,
// Field present but null → None; absent → None
// Both cases handled identically by Option<T>
pub avatar_url: Option<String>,
// Omit from serialized JSON if None (don't write "avatar_url": null)
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<String>,
// Provide a non-None default if field is absent
#[serde(default = "default_role")]
pub role: String,
}
fn default_role() -> String { "viewer".to_string() }
Without #[serde(default)], a missing JSON field for a non-Option type causes a deserialization error. With #[serde(default)], it uses the Default trait — Option<T> defaults to None, integers to 0, strings to "".
Serde's enum representation options map directly to common JSON polymorphism patterns:
use serde::{Deserialize, Serialize};
// Externally tagged (serde default): {"Click": {"x": 100, "y": 200}}
#[derive(Serialize, Deserialize)]
enum Event {
Click { x: i32, y: i32 },
Keypress { key: String, ctrl: bool },
Submit { form_id: String },
}
// Internally tagged: {"type": "click", "x": 100, "y": 200}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum InternalEvent {
Click { x: i32, y: i32 },
Keypress { key: String, ctrl: bool },
Submit { form_id: String },
}
// Adjacently tagged: {"t": "click", "c": {"x": 100, "y": 200}}
#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum AdjacentEvent {
Click { x: i32, y: i32 },
Keypress { key: String },
}
// Untagged: serde tries each variant in order
// Use when variants have clearly distinct shapes
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum UntaggedValue {
Integer(i64),
Float(f64),
Text(String),
Bool(bool),
}
Internally tagged (#[serde(tag = "type")]) matches the most common API pattern where a "type" field discriminates the variant. Serde uses it as a fast discriminator without trying all variants in order.
use serde::{Deserialize, Deserializer};
fn deserialize_comma_separated<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?;
Ok(s.split(',').map(|t| t.trim().to_string()).collect())
}
fn deserialize_cents_to_dollars<'de, D>(deserializer: D) -> Result<f64, D::Error>
where D: Deserializer<'de> {
let cents = u64::deserialize(deserializer)?;
Ok(cents as f64 / 100.0)
}
#[derive(Deserialize)]
pub struct Product {
pub sku: String,
pub name: String,
// JSON: "tags": "rust,systems,fast"
// Rust: Vec<String>
#[serde(deserialize_with = "deserialize_comma_separated")]
pub tags: Vec<String>,
// JSON: "price_cents": 4999
// Rust: 49.99 f64
#[serde(rename = "price_cents", deserialize_with = "deserialize_cents_to_dollars")]
pub price: f64,
}
For high-throughput deserialization where allocating a new String for every field is too expensive, serde supports borrowing from the input buffer:
use serde::Deserialize;
// &'de str borrows from the input JSON bytes — no allocation
// Constraint: the input JSON must outlive the deserialized struct
#[derive(Deserialize)]
pub struct EventRef<'de> {
pub id: u64,
pub name: &'de str, // zero-copy string reference into JSON input
pub tags: Vec<&'de str>,
}
// Usage: input JSON must remain valid while EventRef is in scope
let json = r#"{"id": 1, "name": "click", "tags": ["ui"]}"#;
let event: EventRef = serde_json::from_str(json)?;
println!("{}", event.name); // "click" — no String allocation
Zero-copy deserialization cuts allocation overhead by ~30-50% for read-heavy workloads. The trade-off is lifetime management — the struct borrows from the JSON input and cannot outlive it.
use axum::{extract::Json, http::StatusCode, response::IntoResponse, routing::post, Router};
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Debug, Deserialize, Validate)]
struct CreateUser {
#[validate(length(min = 3, max = 50))]
username: String,
#[validate(email)]
email: String,
#[validate(length(min = 8))]
password: String,
}
#[derive(Serialize)]
struct UserResponse {
id: u64,
username: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> impl IntoResponse {
if let Err(errors) = payload.validate() {
return (StatusCode::BAD_REQUEST, Json(errors)).into_response();
}
// payload.username, payload.email — validated at runtime by validator crate
let user = UserResponse { id: 1, username: payload.username, email: payload.email };
(StatusCode::CREATED, Json(user)).into_response()
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/users", post(create_user));
axum::serve(tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(), app).await.unwrap();
}
u32 / u64 for IDs and counts, never f64: JSON numbers are IEEE 754 floats, but serde parses them into the Rust type you specify. An ID stored as f64 silently loses precision for integers larger than 2^53.#[serde(deny_unknown_fields)] to strict input types: On public API request bodies, unknown fields indicate a client bug or version mismatch. Catching them early produces better error messages than silently ignoring them.serde_json::Value sparingly: Value allocates a tree for the entire JSON document. Use it for truly dynamic data; for semi-structured data, define structs with serde_json::Value only for the dynamic sub-fields.chrono in Cargo.toml: chrono = { version = "0.4", features = ["serde"] } — the serde feature enables DateTime<Utc> deserialization from ISO 8601 strings. Without it, you get a compile error at the derive macro expansion site.Q: How do I handle a JSON field named with a Rust keyword (e.g., "type")?
A: Use #[serde(rename = "type")] on a field named something valid in Rust: #[serde(rename = "type")] pub kind: EventKind. Rust allows you to name the field anything; serde controls the JSON key name.
Q: How do I deserialize a JSON number that might be a string?
A: Use #[serde(deserialize_with)] with a function that tries u64::deserialize first, then falls back to parsing a string. The serde_with crate has a serde_with::DisplayFromStr helper for exactly this case.
Q: What is the performance difference between serde_json and simd_json?
A: simd_json uses SIMD CPU instructions to parse JSON 2-4× faster than serde_json. It's a drop-in replacement for most use cases: swap serde_json::from_str for simd_json::serde::from_str. Use it for hot paths parsing large JSON payloads on modern CPUs.
Q: How do I serialize an enum variant as just a string (not an object)?
A: For unit variants (no fields), serde serializes them as their name string by default: Status::Active → "Active". Add #[serde(rename_all = "snake_case")] to get "active". For tuple variants you want as strings, use the serde_with::SerializeDisplay / DeserializeFromStr helpers.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.