serde_mask

Crates.ioserde_mask
lib.rsserde_mask
version0.1.1
created_at2026-01-07 23:30:09.811232+00
updated_at2026-01-07 23:44:15.355368+00
descriptionMask sensitive data during serde serialization for LLM ingestion
homepage
repositoryhttps://github.com/nestyy1337/serde_mask
max_upload_size
id2029255
size31,915
nesty (nestyy1337)

documentation

README

serde_mask

Mask sensitive data during serde serialization for LLM ingestion, with secrets attached to the object, not a separate struct like expunge does.

The crate intercepts serde serialization and substitutes sensitive values with placeholders. A deanonymize method on the struct lets you replace placeholders in LLM responses with the original values.

Usage

use serde_mask::{anonymize, Anonymize, AnonymizeTrait};

#[anonymize]
#[derive(Debug, Anonymize)]
struct Query {
    #[anon]
    username: String,
    public: String,
}

let q = Query {
    username: "my_secret_username".to_string(),
    public: "visible".to_string(),
    __state: std::sync::OnceLock::new(),
};

// Serializes with secret masked
let json = serde_json::to_string(&q).unwrap();
// {"secret":"ANON_123456","public":"visible"}

// LLM responds with something like "Contact user ANON_123456 immediately."
// then we deanonymize it back to "Contact user my_secret_username immediately."
let response = "Contact user ANON_123456 immediately.";
let restored = q.deanonymize(response.to_string());
// "Contact user my_secret_username immediately."

Custom types

Implement AnonymizeTrait for your own types:

use serde_mask::AnonymizeTrait;

struct Email(String);

impl AnonymizeTrait for Email {
    type State = String;

    fn anonymize(&self) -> Self::State {
        format!("EMAIL_{}", fastrand::usize(0..1_000_000))
    }

    fn deanonymize(&self, state: Self::State, serialized: &str) -> String {
        serialized.replace(&state, &self.0)
    }
}

Design notes

The state is stored in a OnceLock field on the struct itself. This means the anonymization mapping is computed once on first serialize and reused, so deanonymize always uses the same placeholders that were serialized.

Alternative approaches considered:

  • Separate derived struct (like snafu/expunge): more complex, requires conversion
  • Thread locals: doesn't work well with async

Crate is breaking invariant of types, for example:

struct Age {
    age: usize
}

age type would get converted to String type at serialization. For JSON serde would wrap the value with double quotes, but I bet LLM wouldn't notice.

Commit count: 4

cargo fmt