json-steroids 0.2.0

Blazing fast JSON serialization and deserialization library, powered by procedural macros.
Documentation

json-steroids 🚀

A high-performance, zero-copy JSON parsing and serialization library for Rust with derive macros for automatic implementation.

Rust License

Features

  • Zero-copy parsing - Strings without escape sequences are borrowed directly from input, avoiding unnecessary allocations
  • Fast serialization - Pre-allocated buffers with efficient string escaping and number formatting
  • Derive macros - Automatically generate serializers and deserializers for your types
  • Minimal dependencies - Only uses itoa and ryu for fast number formatting
  • Full JSON support - Handles all JSON types including Unicode escape sequences and surrogate pairs
  • Pretty printing - Optional indented output for human-readable JSON
  • Dynamic values - Parse JSON into a flexible JsonValue type when structure is unknown

Installation

Add to your Cargo.toml:

[dependencies]
json-steroids = "0.1.2"

Quick Start

use json_steroids::{Json, to_string, from_str};

#[derive(Debug, Json, PartialEq)]
struct Person {
    name: String,
    age: u32,
    email: Option<String>,
}

fn main() {
    // Serialize
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
        email: Some("alice@example.com".to_string()),
    };
    let json = to_string(&person);
    println!("{}", json);
    // Output: {"name":"Alice","age":30,"email":"alice@example.com"}

    // Deserialize
    let json_str = r#"{"name":"Bob","age":25,"email":null}"#;
    let person: Person = from_str(json_str).unwrap();
    println!("{:?}", person);
}

Performance Benchmarks

Comprehensive benchmark comparison between json-steroids and serde_json. All benchmarks measured with Criterion on Apple M4 Max.

Category Benchmark Description json-steroids serde_json Result
🚀 Zero-Copy borrowed_str_struct_deserialize Struct with 3 &str fields (zero-copy) 35.9 ns 59.4 ns ✅ 1.7x faster
🚀 Zero-Copy cow_struct_deserialize Struct with 4 Cow fields (zero-copy) 67.1 ns 126.6 ns ✅ 1.9x faster
🚀 Zero-Copy zero_copy_vec_strings Vec of 4 Cow strings (zero-copy) 102.9 ns 274.6 ns ✅ 2.7x faster
🚀 Zero-Copy cow_struct_serialize Serialize struct with Cow fields 52.0 ns 95.9 ns ✅ 1.8x faster
ðŸ“Ī Serialization serialize_simple Simple struct (3 fields: String, i64, bool) 21.9 ns 36.3 ns ✅ 1.7x faster
ðŸ“Ī Serialization serialize_complex Complex nested struct with arrays 94.3 ns 162.3 ns ✅ 1.7x faster
ðŸ“Ī Serialization many_fields_serialize Struct with 15 mixed-type fields 107.9 ns 203.0 ns ✅ 1.9x faster
ðŸ“Ī Serialization large_array_serialize Array of 1000 integers 2.78 Ξs 2.92 Ξs ✅ 1.05x faster
ðŸ“Ĩ Deserialization deserialize_simple Simple struct (3 fields: String, i64, bool) 41.7 ns 50.4 ns ✅ 1.2x faster
ðŸ“Ĩ Deserialization deserialize_complex Complex nested struct with arrays 273.3 ns 365.5 ns ✅ 1.3x faster
ðŸ“Ĩ Deserialization many_fields_deserialize Struct with 15 mixed-type fields 261.5 ns 352.4 ns ✅ 1.3x faster
ðŸ“Ĩ Deserialization large_array_deserialize Array of 1000 integers 3.88 Ξs 6.48 Ξs ✅ 1.7x faster
🔄 Round-Trip roundtrip_complex Serialize + deserialize complex struct 395.2 ns 533.8 ns ✅ 1.4x faster
ðŸŽŊ Dynamic parse_dynamic Parse to JsonValue (structure unknown) 211.7 ns 330.5 ns ✅ 1.6x faster
ðŸŽŊ Dynamic deeply_nested_parse Parse deeply nested JSON (64+ levels) 426.5 ns 666.2 ns ✅ 1.6x faster
ðŸ“Ī Serialization string_serialize_with_escapes String with escape sequences (\n, \t, etc.) 46.4 ns 43.4 ns ⚠ïļ 0.94x
ðŸ“Ī Serialization string_serialize_no_escapes Plain string without escapes 30.3 ns 28.8 ns ⚠ïļ 0.95x

Performance Summary

  • ✅ json-steroids wins: 14 of 17 benchmarks (82%)
  • 🚀 1.7-2.7x faster for zero-copy operations with Cow<'de, str> and &'de str
  • ⚡ 21-88% faster for typical serialize/deserialize workloads
  • 📊 67% faster for large array deserialization
  • ðŸŽŊ Best use cases: Zero-copy strings, structs, arrays, complex structures, dynamic parsing
  • ⚠ïļ serde_json wins: String escaping (6%), simple strings (5%)

Why Choose json-steroids?

Optimized for real-world use cases:

  • 🚀 Zero-copy string parsing - Strings without escape sequences are borrowed directly (Cow::Borrowed)
  • ðŸ“Ķ Complex nested structures - 20-35% faster serialization and deserialization
  • 🔍 Dynamic JSON parsing - 60% faster when structure is unknown
  • ðŸŽŊ Type-safe numbers - Specific methods for each integer/float type (no implicit conversions)
  • ðŸ’ū Memory efficient - Pre-allocated buffers, minimal reallocations
  • 🛠ïļ Production ready - Handles Unicode escapes, surrogate pairs, all JSON edge cases

Note: Run cargo bench to measure performance on your hardware. Results may vary based on CPU architecture and workload patterns.

Key Performance Features

  • Zero-copy string parsing - Strings without escape sequences are borrowed directly, avoiding allocations
  • Cow<'de, str> support - True zero-copy deserialization with Cow::Borrowed for strings without escapes
  • Fast number formatting - Uses itoa and ryu for optimized integer and float serialization
  • Efficient memory management - Pre-allocated buffers minimize reallocations
  • Optimized string escaping - Fast-path detection for strings that don't need escaping
  • Minimal overhead - Streamlined trait implementations with no unnecessary abstractions

Zero-Copy Deserialization

json-steroids supports true zero-copy deserialization using Cow<'de, str>:

use json_steroids::from_str;
use std::borrow::Cow;

// Zero-copy: strings without escape sequences
let json = r#""hello world""#;
let result: Cow<str> = from_str(json).unwrap();
assert!(matches!(result, Cow::Borrowed(_))); // No allocation!

// In collections - zero-copy for all elements without escapes
let json = r#"["apple","banana","cherry"]"#;
let result: Vec<Cow<str>> = from_str(json).unwrap();
// All three elements are Cow::Borrowed - zero allocations!

// Automatic handling of escapes
let json = r#""hello\nworld""#;
let result: Cow<str> = from_str(json).unwrap();
assert!(matches!(result, Cow::Owned(_))); // Owned only when necessary

Performance advantage: json-steroids with Cow<'de, str> is ~30% faster than serde_json's zero-copy mode and ~2x faster for serialization!

Running Benchmarks

To run benchmarks on your own system:

cargo bench

View the detailed HTML report:

open target/criterion/report/index.html

Derive Macros

#[derive(Json)]

The combined derive macro that implements both JsonSerialize and JsonDeserialize:

use json_steroids::Json;

#[derive(Json)]
struct User {
    id: u64,
    username: String,
    active: bool,
}

#[derive(JsonSerialize)] and #[derive(JsonDeserialize)]

Use these when you only need one direction:

use json_steroids::{JsonSerialize, JsonDeserialize};

#[derive(JsonSerialize)]
struct LogEntry {
    timestamp: u64,
    message: String,
}

#[derive(JsonDeserialize)]
struct Config {
    host: String,
    port: u16,
}

Field Renaming

Use the #[json(rename = "...")] attribute to customize field names in JSON:

use json_steroids::Json;

#[derive(Json)]
struct ApiResponse {
    #[json(rename = "statusCode")]
    status_code: u32,
    #[json(rename = "errorMessage")]
    error_message: Option<String>,
}

Enum Support

Enums are fully supported with different representations:

use json_steroids::Json;

// Unit variants serialize as strings
#[derive(Json)]
enum Status {
    Active,    // "Active"
    Inactive,  // "Inactive"
    Pending,   // "Pending"
}

// Tuple and struct variants use object notation
#[derive(Json)]
enum Message {
    Text(String),                    // {"Text":["hello"]}
    Coordinates { x: i32, y: i32 },  // {"Coordinates":{"x":10,"y":20}}
}

API Reference

Serialization Functions

// Compact JSON output
pub fn to_string<T: JsonSerialize>(value: &T) -> String;

// Pretty-printed JSON with 2-space indentation
pub fn to_string_pretty<T: JsonSerialize>(value: &T) -> String;

Deserialization Functions

// Parse from string slice
pub fn from_str<T: JsonDeserialize>(s: &str) -> Result<T>;

// Parse from bytes
pub fn from_bytes<T: JsonDeserialize>(bytes: &[u8]) -> Result<T>;

Dynamic Parsing

When the JSON structure isn't known at compile time:

use json_steroids::{parse, JsonValue};

let json = r#"{"name": "test", "values": [1, 2, 3]}"#;
let value = parse(json).unwrap();

// Access fields using indexing
assert_eq!(value["name"].as_str(), Some("test"));
assert!(value["values"].is_array());
assert_eq!(value["values"][0].as_i64(), Some(1));

// Check types
assert!(value.is_object());
assert!(value["missing"].is_null()); // Missing fields return null

JsonValue Type

The JsonValue enum represents any JSON value:

pub enum JsonValue {
    Null,
    Bool(bool),
    Integer(i64),
    Float(f64),
    String(String),
    Array(Vec<JsonValue>),
    Object(Vec<(String, JsonValue)>),
}

Methods available on JsonValue:

  • Type checking: is_null(), is_bool(), is_number(), is_string(), is_array(), is_object()
  • Value extraction: as_bool(), as_i64(), as_u64(), as_f64(), as_str(), as_array(), as_object()
  • Ownership: into_string(), into_array(), into_object()
  • Indexing: value["key"] for objects, value[0] for arrays

Supported Types

Primitives

  • Booleans: bool
  • Integers: i8, i16, i32, i64, isize, u8, u16, u32, u64, usize
  • Floats: f32, f64

Strings

  • String
  • &str (serialize only)
  • Cow<str>

Collections

  • Vec<T>
  • [T; N] (arrays, serialize only)
  • HashMap<K, V> (K must be string-like)
  • BTreeMap<K, V> (K must be string-like)

Wrapper Types

  • Option<T> - Serializes as null when None
  • Box<T>

Tuples

Tuples up to 8 elements are supported and serialize as JSON arrays:

let tuple = (1, "hello", true);
let json = to_string(&tuple); // [1,"hello",true]

Error Handling

The library provides detailed error messages:

use json_steroids::{from_str, JsonError};

let result: Result<i32, _> = from_str("not a number");
match result {
    Ok(value) => println!("Parsed: {}", value),
    Err(JsonError::ExpectedToken(expected, pos)) => {
        println!("Expected {} at position {}", expected, pos);
    }
    Err(e) => println!("Error: {}", e),
}

Error types include:

  • UnexpectedEnd - Input ended unexpectedly
  • UnexpectedChar(char, usize) - Unexpected character at position
  • ExpectedChar(char, usize) - Expected specific character
  • ExpectedToken(&str, usize) - Expected token (e.g., "string", "number")
  • InvalidNumber(usize) - Invalid number format
  • InvalidEscape(usize) - Invalid escape sequence
  • InvalidUnicode(usize) - Invalid Unicode escape
  • InvalidUtf8 - Invalid UTF-8 encoding
  • MissingField(String) - Required field missing during deserialization
  • UnknownVariant(String) - Unknown enum variant
  • TypeMismatch - Type mismatch during deserialization
  • NestingTooDeep(usize) - JSON nesting exceeds maximum depth (128)

Performance

json-steroids is designed for high performance:

Zero-Copy Parsing

Strings that don't contain escape sequences are borrowed directly from the input buffer using Cow<str>, avoiding allocation:

// This string has no escapes - zero allocation!
let json = r#"{"name": "hello world"}"#;

// This string has escapes - allocation needed to unescape
let json = r#"{"name": "hello\nworld"}"#;

Fast Number Formatting

Uses the itoa and ryu crates for extremely fast integer and floating-point formatting.

Efficient String Escaping

The serializer uses a fast path that checks if escaping is needed before processing:

// Fast path - no escaping needed
let s = "hello world";

// Slow path - escaping required
let s = "hello\nworld";

Pre-allocated Buffers

The JsonWriter pre-allocates buffer space to minimize reallocations during serialization.

Architecture

json-steroids/
├── src/
│   ├── lib.rs       # Public API and re-exports
│   ├── parser.rs    # Zero-copy JSON parser
│   ├── writer.rs    # Fast JSON serializer
│   ├── value.rs     # Dynamic JsonValue type
│   ├── traits.rs    # JsonSerialize/JsonDeserialize traits + impls
│   └── error.rs     # Error types
└── json-steroids-derive/
    └── src/
        └── lib.rs   # Procedural macros

Examples

Nested Structures

use json_steroids::Json;

#[derive(Json)]
struct Address {
    street: String,
    city: String,
    country: String,
}

#[derive(Json)]
struct Company {
    name: String,
    address: Address,
    employees: Vec<String>,
}

let company = Company {
    name: "Acme Corp".to_string(),
    address: Address {
        street: "123 Main St".to_string(),
        city: "Springfield".to_string(),
        country: "USA".to_string(),
    },
    employees: vec!["Alice".to_string(), "Bob".to_string()],
};

let json = to_string(&company);

Working with Optional Fields

use json_steroids::{Json, from_str};

#[derive(Json, Debug)]
struct UserProfile {
    username: String,
    bio: Option<String>,
    age: Option<u32>,
}

// Missing optional fields default to None
let json = r#"{"username": "alice"}"#;
let profile: UserProfile = from_str(json).unwrap();
assert!(profile.bio.is_none());
assert!(profile.age.is_none());

// Explicit null also becomes None
let json = r#"{"username": "bob", "bio": null, "age": 25}"#;
let profile: UserProfile = from_str(json).unwrap();
assert!(profile.bio.is_none());
assert_eq!(profile.age, Some(25));

Pretty Printing

use json_steroids::{Json, to_string_pretty};

#[derive(Json)]
struct Config {
    debug: bool,
    port: u16,
}

let config = Config { debug: true, port: 8080 };
let json = to_string_pretty(&config);
// Output:
// {
//   "debug": true,
//   "port": 8080
// }

Custom Serialization with JsonWriter

For advanced use cases, you can use JsonWriter directly:

use json_steroids::JsonWriter;

let mut writer = JsonWriter::new();
writer.begin_object();
writer.write_key("name");
writer.write_string("custom");
writer.write_comma();
writer.write_key("values");
writer.begin_array();
writer.write_i64(1);
writer.write_comma();
writer.write_i64(2);
writer.end_array();
writer.end_object();

let json = writer.into_string();
// {"name":"custom","values":[1,2]}

Running Benchmarks

cargo bench

Running Tests

cargo test

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.