json-escape 0.1.1

A high-performance, `no_std` library for streaming JSON string escaping and unescaping. Process large JSON strings with zero-copy slicing and no intermediate allocations, ideal for parsers and memory-constrained environments.
Documentation

json-escape

crates.io docs.rs CI

A high-performance, no_std compatible library for streaming JSON string escaping and unescaping. Process large JSON strings with zero-copy slicing and no intermediate allocations, ideal for parsers and memory-constrained environments. ⚡

The core of the library is two iterator-based structs:

  • Escape: Lazily yields escaped string slices from an input &str.
  • Unescape: Lazily yields unescaped byte slices from an input &[u8].

This streaming approach avoids allocating a single large String for the result, making it incredibly efficient for large data and I/O operations.


Key Features

  • 🚀 Streaming & Iterator-based: Process data in chunks without buffering the entire result in memory.
  • ✨ Zero-Copy Slicing: For sequences of characters that don't need modification, the iterators yield slices borrowed directly from the input.
  • 🧩 no_std Compatible: Usable in embedded systems and other memory-constrained environments (with alloc for owned conversions).
  • Unicode Excellence: Correctly decodes \uXXXX sequences, including full support for UTF-16 surrogate pairs (e.g., \uD83D\uDE00 for 😀).
  • 🔎 Robust Error Handling: The Unescape iterator returns descriptive errors for invalid or truncated escape sequences.
  • std::io Integration: With the std feature, Unescape implements std::io::Read, allowing it to be plugged directly into APIs that consume readers (like serde_json).

Quick Start

Escaping a String

use json_escape::escape_str;
use std::borrow::Cow;

let input = "Hello, \"world\"!\nThis is a backslash: \\";
let expected = r#"Hello, \"world\"!\nThis is a backslash: \\"#;

// escape_str returns an iterator. Collect it into a String.
let escaped_string: String = escape_str(input).collect();
assert_eq!(escaped_string, expected);

// For efficiency, convert to a Cow<str>. This avoids allocation
// if the input string requires no escaping.
let cow: Cow<str> = escape_str("no escapes needed").into();
assert!(matches!(cow, Cow::Borrowed(_)));

Unescaping a String

use json_escape::unescape;
use std::borrow::Cow;

let input = r#"Emoji: \uD83D\uDE00 and a tab\t!"#;
let expected = "Emoji: 😀 and a tab\t!";

// unescape returns an iterator over Result<&[u8], _>.
// The `decode_utf8` helper collects and validates the output.
let decoded_cow: Cow<str> = unescape(input).decode_utf8().unwrap();
assert_eq!(decoded_cow, expected);

Advanced Usage: Zero-Allocation REST API Parsing

A common scenario in web services is receiving a JSON payload where one of the fields is another JSON object, escaped as a string.

{
  "transaction_id": "txn_123",
  "payload": "{\"user_id\": 42, \"items\": [\"apple\", \"orange\"], \"metadata\": {\"source\": \"mobile\"}}"
}

The standard approach requires allocating a new String to hold the unescaped payload before parsing it. This is inefficient, especially for large payloads.

json-escape avoids this entirely by plugging its streaming Unescape reader directly into serde_json.

The json-escape Solution: No Intermediate Allocation!

use json_escape::unescape_quoted;
use serde::Deserialize;
use serde_json::RawValue;

// The inner payload we want to extract and parse.
#[derive(Deserialize, Debug)]
struct InnerPayload {
    user_id: u64,
    items: Vec<String>,
}

// The outer structure. We use `&RawValue` for a zero-copy view.
#[derive(Deserialize)]
struct OuterPayload<'a> {
    transaction_id: String,
    #[serde(borrow)]
    payload: &'a RawValue,
}

fn main() {
    let response_body = r#"{
        "transaction_id": "txn_123",
        "payload": "{\"user_id\": 42, \"items\": [\"apple\", \"orange\"]}"
    }"#;

    // 1. Parse the outer frame without allocating for `payload`.
    let outer: OuterPayload = serde_json::from_str(response_body).unwrap();

    // 2. Create a streaming reader from the raw, escaped payload.
    // This implements `std::io::Read`.
    let reader = unescape_quoted(outer.payload.get());

    // 3. Parse the inner payload directly from the reader.
    // NO intermediate `String` is ever allocated for the unescaped payload!
    let inner: InnerPayload = serde_json::from_reader(reader).unwrap();

    assert_eq!(inner.user_id, 42);
    assert_eq!(inner.items, vec!["apple", "orange"]);
    println!("Successfully parsed inner payload: {:?}", inner);
}

Installation

Add this to your Cargo.toml:

[dependencies]
json-escape = "0.1.1"

Feature Flags

  • alloc (enabled by default): Provides Cow, String, and Vec conversions.
  • std (enabled by default): Provides std::io::Read and std::error::Error implementations.

For no_std environments without an allocator, use:

[dependencies]
json-escape = { version = "*", default-features = false }

License

This project is licensed under either of

at your option.