qubit-redact-derive
Qubit Redact Derive provides procedural macros for the
qubit-redact runtime crate. Use
them to define a deliberate redaction boundary for Rust domain objects: create
safe borrowed diagnostic views with Redact, or explicitly replace logical
values with RedactMut. A single #[derive(Redact)] generates both runtime
capabilities by default.
Why qubit-redact-derive
- Field annotations make masking, omission, nested redaction, and map redaction reviewable at the domain-model boundary.
- The macros support named, tuple, and unit structs, plus enums with all three variant shapes.
- Optional Serde support can make direct serialization of the original type produce a redacted representation without granting it deserialization or exposing an original-value escape hatch.
- The generated code resolves a direct
qubit-redactdependency, including a Cargo-renamed dependency, instead of relying on a fixed import spelling.
Quick Start
Add the runtime and derive crates together:
[]
= "0.4"
= "0.4"
use Redact as _;
use Redact;
Redact creates a borrowed view. The original Credentials value remains
available to application logic. The same derive also generates the
RedactMut capability:
use ;
use Redact;
Use #[redact(no_mut)] when a type contains sensitive borrowed fields that
cannot be replaced in place.
For types whose fields must all be reviewed explicitly, add
#[redact(require_explicit)]. Mark intentionally visible fields with
#[redact(plain)]; the default behavior remains unchanged for existing types.
Choose a Derive
| Need | Derive/attribute | Result |
|---|---|---|
| Safely inspect or log a domain object without changing it | Redact |
A borrowed Redacted<T> view. |
| Replace owned logical values before another boundary | Redact |
redact_in_place() or into_redacted() from the generated RedactMut capability. |
| Make an original type format through the process default policy | Redact with #[redact(debug)] or #[redact(display)] |
Generated Debug and/or Display for the original type. |
| Serialize the original type as redacted JSON | Redact with #[redact(serde)] |
Direct redacted Serialize for the original type plus policy-aware serialization for Redacted<T>. |
Use the immutable capability for diagnostics whenever possible. Use the generated mutable capability only when the next boundary requires a logically replaced value.
Attribute Overview
Field attributes select exactly one handling mode:
| Attribute | Effect |
|---|---|
#[redact(level = "low")], "medium", "high", or "secret" |
Masks the field with the specified runtime sensitivity. |
#[redact(plain)] |
Keeps the field visible and documents the intentional pass-through. |
#[redact(skip)] |
Omits the field from the redacted view. |
#[redact(nested)] |
Delegates redaction to the nested value. |
#[redact(map)] |
Redacts text-keyed map values using their keys and the complete runtime policy. |
#[redact(json)] |
Redacts JSON stored in a String recursively by object key; invalid JSON is replaced opaquely. |
Container attributes are opt-in controls:
| Attribute | Effect |
|---|---|
#[redact(debug)] |
Generates redacted Debug for the original type. |
#[redact(display)] |
Generates redacted Display for the original type. |
#[redact(serde)] |
Generates direct redacted Serialize for the original type and preserves policy-aware serialization for Redacted<T>. |
#[redact(no_mut)] |
Does not generate RedactMut; useful for sensitive borrowed fields such as &str. |
#[redact(require_explicit)] |
Requires every field to select one field mode; it does not change the default behavior. |
The options can be combined in one attribute:
Unmarked fields use their ordinary Debug representation by default. They are
neither masked nor recursively traversed. require_explicit changes only the
derive invocation where it is written.
Field sensitivity classification belongs to the downstream application and its
domain-model owners. This derive cannot determine whether a field is sensitive
in a particular product, so it intentionally does not require every field to
carry an attribute. Applications should mark the fields that cross their
redaction boundary and choose plain only when ordinary visibility is an
intentional, reviewed decision. require_explicit is available for models
whose review policy requires every field to make that choice.
When #[redact(serde)] is enabled, deserialization-only Serde controls such as
default, alias, skip_deserializing, and deny_unknown_fields are accepted
and ignored by the generated serialization. Structural or serialization-side
controls that could bypass redaction remain rejected.
Serialization adapters (with and serialize_with) are accepted only on
plain or skip fields. A plain adapter intentionally receives the original
field value; redaction modes that inspect raw state reject adapters so they
cannot bypass the generated redaction.
Dependencies and Features
The generated code requires qubit-redact to be a direct dependency. The
derive crate discovers Cargo renames, so this also works:
[]
= { = "qubit-redact", = "0.4" }
= "0.4"
To use #[redact(serde)], enable the runtime crate's serde feature and
declare serde directly:
[]
= { = "0.4", = ["serde"] }
= "0.4"
= { = "1", = ["derive"] }
= "1"
#[redact(json)] requires the runtime crate's json feature. It formats a
redacted JSON view, rewrites the string as compact redacted JSON for
RedactMut, and serializes as a JSON string when combined with
#[redact(serde)].
The derive package's test-json feature is only for its own test suite; it
does not enable runtime features for downstream crates.
Safety Boundaries
- The macros protect only the redacted view, generated formatting, or explicit in-place operation that you use. They cannot protect unrelated log calls or serialization paths.
- An unmarked field uses its own
Debugoutput. Mark every field whose representation can disclose sensitive data, or opt into#[redact(require_explicit)]and use#[redact(plain)]for intentional pass-through fields. skipomits a value from the redacted representation; it does not erase the original value.RedactMutperforms logical replacement only. It does not erase released allocations, aliases, copies, or borrowed backing storage.debug,display, and directserdeserialization use the process-wide default policy. Use an explicitredacted_withboundary when a call site needs policy isolation. RedactedDebugoutput uses the policy diagnostic output budget by default. Nested, map, and JSON fields share the same diagnostic session for one redacted view, so they cannot independently reset that budget.- Do not use
skip_serializing_ifwithlevel,nested,map, orjson; its predicate receives the raw field and could reveal sensitive state through field presence. It is supported only withplainandskipfields.
Learn More
- English User Guide and 中文用户手册
- Runtime README and runtime user guide
- Runtime API documentation
- Derive API documentation
Testing
# Run tests with the default feature set
# Run tests with all declared features
# Project CI checks
# Check code coverage
License
Copyright (c) 2025 - 2026. Haixing Hu. All rights reserved.
Licensed under the Apache License, Version 2.0. See LICENSE for the full license text.
Contributing
Contributions are welcome. Please follow the Rust API guidelines, keep public
API documentation and tests current, and run ./align-ci.sh to format code and
./ci-check.sh to satisfy CI requirements before submitting a pull request.
Author
Haixing Hu - Qubit Co. Ltd.
Repository: https://github.com/qubit-ltd/rs-redact-derive