How does serde_json::Value differ from serde_json::RawValue for unstructured JSON handling?

serde_json::Value and serde_json::RawValue serve fundamentally different purposes for handling unstructured JSON. Value is a fully parsed, owned representation that deserializes JSON into a structured enum (Object, Array, String, Number, Bool, Null), enabling traversal and modification but requiring allocation for every element. RawValue is an opaque reference to unparsed JSON text that delays deserialization—it stores the raw string bytes and only parses when explicitly needed. This makes RawValue significantly more efficient for passthrough scenarios where you need to preserve JSON without modifying it, while Value is the right choice when you need to inspect, query, or transform the data.

Basic Value Usage

use serde_json::{Value, json};
 
fn basic_value() {
    // Create a Value from a JSON literal
    let value: Value = json!({
        "name": "Alice",
        "age": 30,
        "active": true,
        "tags": ["rust", "json"],
        "metadata": null
    });
    
    // Value is fully parsed and traversable
    println!("Name: {}", value["name"]);
    println!("Age: {}", value["age"]);
    println!("First tag: {}", value["tags"][0]);
    
    // Value is mutable
    let mut value = value;
    value["age"] = json!(31);
    value["city"] = json!("Seattle");
    
    // Can traverse dynamically
    if let Some(tags) = value["tags"].as_array() {
        for tag in tags {
            println!("Tag: {}", tag);
        }
    }
    
    // Check types
    if let Some(name) = value["name"].as_str() {
        println!("Name as string: {}", name);
    }
    
    if let Some(age) = value["age"].as_u64() {
        println!("Age as u64: {}", age);
    }
}

Value parses JSON into a structured, traversable, and mutable format.

Basic RawValue Usage

use serde_json::value::RawValue;
use serde_json::json;
 
fn basic_raw_value() {
    // RawValue wraps unparsed JSON text
    let raw: Box<RawValue> = RawValue::from_string(
        r#"{"name": "Alice", "age": 30}"#.to_string()
    );
    
    // The JSON is NOT parsed - just stored as a string
    // You cannot traverse or query it directly
    
    // Access the raw string
    println!("Raw JSON: {}", raw.get());
    
    // Can be serialized without re-parsing
    let output = serde_json::to_string(&raw).unwrap();
    println!("Output: {}", output);
    
    // Parse it when needed
    let parsed: serde_json::Value = serde_json::from_str(raw.get()).unwrap();
    println!("Name: {}", parsed["name"]);
}

RawValue stores JSON as text, delaying parsing until explicitly needed.

Memory Representation

use serde_json::{Value, json};
use serde_json::value::RawValue;
use std::mem::size_of;
 
fn memory_representation() {
    // Value is a recursive enum - each element allocates
    let value: Value = json!({
        "users": [
            {"name": "Alice", "id": 1},
            {"name": "Bob", "id": 2},
            {"name": "Charlie", "id": 3}
        ]
    });
    
    // Value allocates:
    // - 1 Object (HashMap)
    // - 1 Array (Vec)
    // - 3 Objects (for each user)
    // - 6 Strings (names and keys)
    // - 3 Numbers (ids)
    // Total: many heap allocations
    
    // RawValue is a single allocation
    let raw: Box<RawValue> = RawValue::from_string(
        r#"{"users":[{"name":"Alice","id":1},{"name":"Bob","id":2},{"name":"Charlie","id":3}]}"#.to_string()
    );
    
    // RawValue is just a string reference
    println!("RawValue size: {} bytes", size_of::<Box<RawValue>>());
    println!("Value size: {} bytes", size_of::<Value>());
    // Note: size_of only measures stack size, not heap allocations
    
    // RawValue's heap: just the string
    // Value's heap: all the structures above
}

Value allocates heavily; RawValue stores only the raw string.

Passthrough Performance

use serde_json::{Value, json};
use serde_json::value::RawValue;
use std::time::Instant;
 
fn passthrough_comparison() {
    let json_data = r#"{
        "id": 12345,
        "name": "Test Item",
        "description": "A longer description for testing",
        "tags": ["tag1", "tag2", "tag3"],
        "metadata": {
            "created": "2024-01-01",
            "modified": "2024-01-15",
            "author": "system"
        },
        "values": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    }"#;
    
    let iterations = 10_000;
    
    // Value: parse, then serialize
    let start = Instant::now();
    for _ in 0..iterations {
        let value: Value = serde_json::from_str(json_data).unwrap();
        let _output = serde_json::to_string(&value).unwrap();
    }
    let value_time = start.elapsed();
    
    // RawValue: store raw, serialize directly
    let start = Instant::now();
    for _ in 0..iterations {
        let raw: Box<RawValue> = serde_json::from_str(json_data).unwrap();
        let _output = serde_json::to_string(&raw).unwrap();
    }
    let raw_time = start.elapsed();
    
    println!("Value passthrough: {:?}", value_time);
    println!("RawValue passthrough: {:?}", raw_time);
    // RawValue is typically much faster for passthrough
}

RawValue is significantly faster for passthrough scenarios.

Deserializing with RawValue

use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
 
// Common pattern: some fields known, others passed through
#[derive(Serialize, Deserialize)]
struct Envelope {
    id: u64,
    timestamp: i64,
    // The payload is kept as raw JSON
    #[serde(bound = "")]
    payload: Box<RawValue>,
}
 
#[derive(Serialize, Deserialize)]
struct UserPayload {
    name: String,
    email: String,
}
 
fn envelope_pattern() {
    let json = r#"{
        "id": 123,
        "timestamp": 1704067200,
        "payload": {"name": "Alice", "email": "alice@example.com"}
    }"#;
    
    // Parse the envelope, but NOT the payload
    let envelope: Envelope = serde_json::from_str(json).unwrap();
    
    println!("ID: {}", envelope.id);
    println!("Timestamp: {}", envelope.timestamp);
    println!("Payload raw: {}", envelope.payload.get());
    
    // Parse payload only when needed
    let payload: UserPayload = serde_json::from_str(envelope.payload.get()).unwrap();
    println!("Name: {}", payload.name);
    println!("Email: {}", payload.email);
    
    // Re-serialize without re-parsing payload
    let output = serde_json::to_string(&envelope).unwrap();
    println!("Output: {}", output);
}

RawValue enables partial deserialization—parse what you need, pass through the rest.

Nested RawValue Patterns

use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
 
#[derive(Serialize, Deserialize)]
struct Message {
    msg_type: String,
    version: u32,
    #[serde(bound = "")]
    data: Box<RawValue>,
}
 
fn nested_raw_value() {
    let messages = vec![
        r#"{"msg_type":"user","version":1,"data":{"name":"Alice"}}"#,
        r#"{"msg_type":"order","version":2,"data":{"items":[1,2,3],"total":99.99}}"#,
        r#"{"msg_type":"event","version":1,"data":{"type":"click","x":100,"y":200}}"#,
    ];
    
    for msg_json in messages {
        let msg: Message = serde_json::from_str(msg_json).unwrap();
        
        match msg.msg_type.as_str() {
            "user" => {
                let user: UserEvent = serde_json::from_str(msg.data.get()).unwrap();
                println!("User: {:?}", user);
            }
            "order" => {
                let order: OrderEvent = serde_json::from_str(msg.data.get()).unwrap();
                println!("Order: {:?}", order);
            }
            "event" => {
                // Keep as raw if we don't need to parse
                println!("Event data: {}", msg.data.get());
            }
            _ => println!("Unknown type: {}", msg.msg_type),
        }
    }
}
 
#[derive(Deserialize)]
struct UserEvent {
    name: String,
}
 
#[derive(Deserialize)]
struct OrderEvent {
    items: Vec<u32>,
    total: f64,
}

RawValue is ideal for message routing where payloads have different structures.

Value for Dynamic Inspection

use serde_json::{Value, json};
 
fn dynamic_inspection() {
    let value: Value = json!({
        "users": [
            {"id": 1, "name": "Alice", "active": true},
            {"id": 2, "name": "Bob", "active": false},
            {"id": 3, "name": "Charlie", "active": true}
        ],
        "count": 3
    });
    
    // Value supports dynamic traversal
    if let Some(users) = value.get("users").and_then(|v| v.as_array()) {
        let active_count = users
            .iter()
            .filter(|user| user.get("active").and_then(|v| v.as_bool()).unwrap_or(false))
            .count();
        println!("Active users: {}", active_count);
    }
    
    // Modify based on conditions
    let mut value = value;
    if let Some(users) = value.get_mut("users").and_then(|v| v.as_array_mut()) {
        for user in users {
            if let Some(name) = user.get("name").and_then(|v| v.as_str()) {
                if name == "Bob" {
                    user["active"] = json!(true);
                }
            }
        }
    }
    
    // Add computed fields
    value["active_count"] = json!(2);
    
    println!("Modified: {}", serde_json::to_string_pretty(&value).unwrap());
}

Value is necessary when you need to inspect and modify JSON dynamically.

RawValue for JSON Logs

use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use std::fs::File;
use std::io::{BufRead, BufReader};
 
#[derive(Deserialize)]
struct LogEntry {
    timestamp: String,
    level: String,
    #[serde(bound = "")]
    message: Box<RawValue>,
}
 
fn json_log_processing() {
    // Simulate reading JSON log lines
    let log_lines = vec![
        r#"{"timestamp":"2024-01-15T10:00:00Z","level":"INFO","message":"Server started"}"#,
        r#"{"timestamp":"2024-01-15T10:00:01Z","level":"DEBUG","message":{"event":"request","path":"/api/users"}}"#,
        r#"{"timestamp":"2024-01-15T10:00:02Z","level":"ERROR","message":{"error":"connection failed","retry":3}}"#,
    ];
    
    for line in log_lines {
        // Parse only the envelope
        let entry: LogEntry = serde_json::from_str(line).unwrap();
        
        // Filter by level without parsing message
        if entry.level == "ERROR" {
            println!("[{}] ERROR: {}", entry.timestamp, entry.message.get());
        }
    }
    
    // This is much faster than parsing each line into Value
    // because we don't parse the message field at all
}

RawValue is efficient for log processing where only some fields need parsing.

Serialization Differences

use serde_json::{Value, json};
use serde_json::value::RawValue;
use serde::{Serialize, Serializer};
 
fn serialization_differences() {
    // Value: serializes from parsed structure
    let value: Value = json!({"name": "Alice", "age": 30});
    let value_json = serde_json::to_string(&value).unwrap();
    println!("Value: {}", value_json);
    
    // RawValue: serializes directly from string
    let raw: Box<RawValue> = RawValue::from_string(
        r#"{"name":"Alice","age":30}"#.to_string()
    );
    let raw_json = serde_json::to_string(&raw).unwrap();
    println!("Raw: {}", raw_json);
    
    // Output is the same, but RawValue doesn't re-parse
    
    // Important: RawValue preserves original formatting
    let pretty_raw: Box<RawValue> = RawValue::from_string(
        r#"{
            "name": "Alice",
            "age": 30
        }"#.to_string()
    );
    println!("Pretty preserved:\n{}", pretty_raw.get());
    
    // This includes whitespace, key ordering, etc.
}

RawValue preserves the original JSON formatting exactly.

Type Safety Considerations

use serde_json::{Value, json};
use serde_json::value::RawValue;
 
fn type_safety() {
    // Value: guaranteed to be valid JSON after parsing
    let value: Value = serde_json::from_str(r#"{"valid": true}"#).unwrap();
    // value is now type-safe JSON
    
    // RawValue: also validated on creation
    let raw: Box<RawValue> = RawValue::from_string(
        r#"{"valid": true}"#.to_string()
    );
    // This validates that it's valid JSON
    
    // But RawValue can hold any valid JSON, no type constraints
    let any_json: Box<RawValue> = RawValue::from_string(
        r#"[1, 2, 3]"#.to_string()
    );
    
    // Dangerous: unsafe constructors
    // RawValue::unsafe_from_string() bypasses validation
    // Only use if you're CERTAIN the string is valid JSON
    
    // Value gives you structural guarantees
    if let Some(obj) = value.as_object() {
        // obj is definitely a JSON object
        for (key, val) in obj {
            println!("{}: {}", key, val);
        }
    }
    
    // RawValue requires re-parsing for structure
    let parsed: Value = serde_json::from_str(raw.get()).unwrap();
    // Now you have structural access
}

Both types validate JSON, but Value provides structural access while RawValue requires re-parsing.

Comparing Use Cases

use serde_json::{Value, json};
use serde_json::value::RawValue;
use serde::{Deserialize, Serialize};
 
// Use Value when:
// 1. Need to inspect/query JSON dynamically
// 2. Need to modify JSON
// 3. Don't know structure at compile time
// 4. Building JSON from scratch
 
fn use_cases_for_value() {
    // Dynamic queries
    let api_response: Value = json!({
        "data": {"users": [{"id": 1}, {"id": 2}]},
        "meta": {"total": 2}
    });
    
    // Query nested paths
    let user_ids: Vec<_> = api_response["data"]["users"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|u| u.get("id").and_then(|v| v.as_u64()))
        .collect();
    
    // Modify structure
    let mut response = api_response;
    response["meta"]["filtered"] = json!(true);
}
 
// Use RawValue when:
// 1. Passthrough JSON without modification
// 2. Partial deserialization (envelope pattern)
// 3. Performance-critical paths
// 4. Preserving original formatting
 
#[derive(Serialize, Deserialize)]
struct ApiRequest {
    id: String,
    #[serde(bound = "")]
    body: Box<RawValue>,
}
 
fn use_cases_for_raw_value() {
    // API proxy: forward body without parsing
    let request: ApiRequest = serde_json::from_str(r#"{
        "id": "req-123",
        "body": {"complex": {"nested": ["data", "here"]}}
    }"#).unwrap();
    
    // Use id, forward body directly
    println!("Processing request: {}", request.id);
    
    // body is still raw - can forward to another service
    let forward_body = request.body.get();
    // No parsing overhead for the complex nested structure
}

Choose Value for manipulation, RawValue for passthrough.

Working with Both

use serde_json::{Value, json};
use serde_json::value::RawValue;
use serde::{Deserialize, Serialize};
 
#[derive(Serialize, Deserialize)]
struct FlexibleEnvelope {
    msg_type: String,
    // Keep as RawValue for efficiency
    #[serde(bound = "")]
    raw_data: Box<RawValue>,
}
 
impl FlexibleEnvelope {
    // Parse into Value when needed
    fn data_as_value(&self) -> Value {
        serde_json::from_str(self.raw_data.get()).unwrap()
    }
    
    // Parse into typed struct when needed
    fn data_as<T: for<'de> Deserialize<'de>>(&self) -> T {
        serde_json::from_str(self.raw_data.get()).unwrap()
    }
    
    // Convert Value back to RawValue
    fn from_value(msg_type: String, data: Value) -> Self {
        let raw_data = RawValue::from_string(serde_json::to_string(&data).unwrap());
        FlexibleEnvelope { msg_type, raw_data }
    }
}
 
fn flexible_handling() {
    let json = r#"{
        "msg_type": "user_created",
        "raw_data": {"user_id": 123, "email": "user@example.com"}
    }"#;
    
    let envelope: FlexibleEnvelope = serde_json::from_str(json).unwrap();
    
    // Keep as raw for forwarding
    println!("Forward: {}", envelope.raw_data.get());
    
    // Parse when needed for processing
    let value = envelope.data_as_value();
    println!("User ID: {}", value["user_id"]);
    
    // Parse into typed struct
    #[derive(Deserialize)]
    struct UserData {
        user_id: u32,
        email: String,
    }
    let typed: UserData = envelope.data_as();
    println!("Typed: {:?}", typed);
}

Convert between Value and RawValue as needed for different operations.

Performance Benchmarks

use serde_json::{Value, json};
use serde_json::value::RawValue;
use std::time::Instant;
 
fn performance_comparison() {
    let large_json = r#"{
        "users": [
            {"id": 1, "name": "Alice", "email": "alice@example.com", "roles": ["admin", "user"]},
            {"id": 2, "name": "Bob", "email": "bob@example.com", "roles": ["user"]},
            {"id": 3, "name": "Charlie", "email": "charlie@example.com", "roles": ["user", "moderator"]}
        ],
        "metadata": {
            "version": "1.0",
            "generated": "2024-01-15T10:00:00Z",
            "count": 3
        }
    }"#;
    
    let iterations = 10_000;
    
    // Parse only
    let start = Instant::now();
    for _ in 0..iterations {
        let _: Value = serde_json::from_str(large_json).unwrap();
    }
    let value_parse = start.elapsed();
    
    let start = Instant::now();
    for _ in 0..iterations {
        let _: Box<RawValue> = serde_json::from_str(large_json).unwrap();
    }
    let raw_parse = start.elapsed();
    
    println!("Parse - Value: {:?}", value_parse);
    println!("Parse - RawValue: {:?}", raw_parse);
    
    // Serialize
    let value: Value = serde_json::from_str(large_json).unwrap();
    let raw: Box<RawValue> = serde_json::from_str(large_json).unwrap();
    
    let start = Instant::now();
    for _ in 0..iterations {
        let _ = serde_json::to_string(&value).unwrap();
    }
    let value_serialize = start.elapsed();
    
    let start = Instant::now();
    for _ in 0..iterations {
        let _ = serde_json::to_string(&raw).unwrap();
    }
    let raw_serialize = start.elapsed();
    
    println!("Serialize - Value: {:?}", value_serialize);
    println!("Serialize - RawValue: {:?}", raw_serialize);
    
    // RawValue typically wins significantly on both operations
}

RawValue outperforms Value significantly for passthrough operations.

Summary Comparison

Aspect Value RawValue
Memory Allocates for each element Single string allocation
Parsing Full parse into structures Validates only, stores raw
Access Direct traversal and indexing Must parse to access content
Mutation Fully mutable Immutable, must re-create
Serialization Reconstructs from structures Outputs stored string
Type safety Enum variants for types Just valid JSON string
Use case Inspect, modify, query Passthrough, partial parse

Synthesis

Value and RawValue serve complementary purposes:

Use Value when:

  • You need to inspect or query JSON dynamically
  • You need to modify the JSON structure
  • You're building JSON from scratch
  • Structure is unknown at compile time
  • You need to validate data types

Use RawValue when:

  • You're passing JSON through without modification
  • Only some fields need parsing (envelope pattern)
  • Performance is critical
  • You want to preserve original formatting
  • You're routing messages with different payload types

Key insight: RawValue is essentially a zero-copy optimization that defers parsing until needed. It trades flexibility for performance—you cannot traverse or modify a RawValue without parsing it first. The envelope pattern (parsing outer structure while keeping inner payload as RawValue) is the canonical use case, enabling efficient routing, filtering, and forwarding of JSON messages without the overhead of fully parsing nested content. For APIs that receive and forward JSON, RawValue can dramatically reduce CPU and memory usage.