Complete Guide to Rust JSON Serialization

Rust ~3 min read
RustJSONSerializationserdePerformance

Master Rust JSON serialization with serde. Learn derive macros, custom serialization, zero-copy deserialization, enum handling, and performance optimization with serde_json.

Serde: The Foundation

Serde is Rust’s serialization framework — zero-cost abstractions with compile-time code generation.

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct User {
    name: String,
    age: u32,
    #[serde(default)] // Optional: Option<T> already deserializes missing fields as None
    email: Option<String>,
    #[serde(rename = "createdAt")]
    created_at: chrono::DateTime<chrono::Utc>,
}

// Serialize
let user = User {
    name: "Alice".to_string(),
    age: 30,
    email: Some("alice@example.com".to_string()),
    created_at: chrono::Utc::now(),
};
let json = serde_json::to_string_pretty(&user)?;

// Deserialize
let parsed: User = serde_json::from_str(&json)?;

Field Attributes

Control serialization behavior at the field level:

use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
struct Config {
    // Rename for JSON compatibility
    #[serde(rename = "databaseUrl")]
    database_url: String,

    // Skip if None
    #[serde(skip_serializing_if = "Option::is_none")]
    optional_field: Option<String>,

    // Default value if missing
    #[serde(default = "default_timeout")]
    timeout: u64,

    // Always skip
    #[serde(skip)]
    internal_state: Vec<u8>,

    // Flatten nested struct
    #[serde(flatten)]
    metadata: HashMap<String, serde_json::Value>,
}

fn default_timeout() -> u64 {
    30
}

Custom Serialization

Implement Serialize and Deserialize for full control:

use serde::{Deserializer, Serializer};
use serde::de::{self, Visitor};
use std::fmt;

struct CustomDate(chrono::NaiveDate);

impl Serialize for CustomDate {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let s = self.0.format("%Y-%m-%d").to_string();
        serializer.serialize_str(&s)
    }
}

impl<'de> Deserialize<'de> for CustomDate {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct DateVisitor;

        impl<'de> Visitor<'de> for DateVisitor {
            type Value = CustomDate;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a date string in YYYY-MM-DD format")
            }

            fn visit_str<E>(self, value: &str) -> Result<CustomDate, E>
            where
                E: de::Error,
            {
                chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")
                    .map(CustomDate)
                    .map_err(de::Error::custom)
            }
        }

        deserializer.deserialize_str(DateVisitor)
    }
}

Enum Serialization

Handle Rust enums in JSON:

#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

// {"type": "Circle", "radius": 5.0}
let circle = Shape::Circle { radius: 5.0 };
let json = serde_json::to_string(&circle)?;

// Externally tagged (default)
#[derive(Serialize, Deserialize)]
enum Color {
    Red,
    Green,
    Blue,
}
// "Red"

// Adjacently tagged
#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum Value {
    Int(i64),
    Text(String),
}
// {"t": "Int", "c": 42}

Error Handling

Robust JSON parsing with proper error types:

use thiserror::Error;

#[derive(Error, Debug)]
enum JsonError {
    #[error("JSON parsing error: {0}")]
    Parse(#[from] serde_json::Error),

    #[error("Validation error: {message}")]
    Validation { message: String },

    #[error("Missing field: {field}")]
    MissingField { field: String },
}

fn parse_config(json: &str) -> Result<Config, JsonError> {
    let config: Config = serde_json::from_str(json)?;

    if config.database_url.is_empty() {
        return Err(JsonError::MissingField {
            field: "databaseUrl".to_string(),
        });
    }

    Ok(config)
}

Performance Optimization

Use simd-json for SIMD-accelerated parsing

use simd_json::prelude::*;

let mut data = json_string.to_owned();
let parsed: User = simd_json::from_str(&mut data)?;
// 2-3x faster than serde_json for large payloads

Avoid allocations with borrow

#[derive(Deserialize)]
struct User<'a> {
    name: &'a str,  // Borrows from input string
    age: u32,
}

let user: User = serde_json::from_str(&json)?;
// No allocation for string fields

Stream processing for large files

use serde_json::Deserializer;

let reader = std::fs::File::open("large.json")?;
let stream = Deserializer::from_reader(reader).into_iter::<User>();

for result in stream {
    let user = result?;
    process_user(user);
}

Working with Dynamic JSON

For unknown structures, use serde_json::Value:

use serde_json::{Value, json};

// Parse into dynamic Value
let data: Value = serde_json::from_str(json_string)?;

// Access fields
let name = data["name"].as_str().unwrap_or("unknown");
let age = data["age"].as_i64().unwrap_or(0);

// Build JSON with the json! macro
let user = json!({
    "name": "Alice",
    "age": 30,
    "tags": ["rust", "json"]
});

// Convert Value back to string
let output = serde_json::to_string_pretty(&user)?;

Untagged Enums

When JSON doesn’t have a type discriminator:

#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Value {
    Int(i64),
    Text(String),
    Bool(bool),
}
// Tries each variant in order: Int, then Text, then Bool

JSON API Comparison Across Languages

Language Library Serialize Deserialize Pretty Print Config
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize<T>(str) WriteIndented = true JsonSerializerOptions
TypeScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Rust serde_json serde_json::to_string(&obj) serde_json::from_str::<T>(str) serde_json::to_string_pretty(&obj) serde attributes, custom (de)serializers

Frequently Asked Questions (FAQ)

How to handle datetime types in JSON serialization?

Most language JSON libraries do not support datetime types by default. The common approach is to serialize as ISO 8601 format strings (e.g. "2026-07-15T10:30:00Z") or Unix timestamps, then convert back to datetime objects during deserialization.

How to ignore null values or empty fields in JSON serialization?

Different languages have different implementations: Python can filter None values in custom encoders; JavaScript can use replacer functions; Java Jackson uses @JsonInclude annotations; Go uses omitempty struct tags; C# sets DefaultIgnoreCondition.

How to handle circular references in JSON serialization?

Circular references are a common pitfall. Standard JSON libraries usually throw exceptions or cause stack overflow. Solutions include using @JsonIdentityInfo (Java Jackson), implementing custom serializers, or converting to DTOs before serialization.

Why are my private fields not being serialized?

JSON serialization libraries typically only serialize public fields or properties with getters. This is a security consideration. You can use annotations like @JsonProperty (Java Jackson) or [JsonInclude] (C#) to include non-public members.

What are the performance optimization tips for JSON serialization?

1) Reuse serializer instances; 2) Use streaming APIs for large data; 3) Use compile-time generation (C# Source Generator, Go easyjson); 4) Avoid excessive nesting; 5) Use database pagination for large JSON.

Recommended Tools

These tools can help you handle JSON data more efficiently in Rust development:

Related Articles