fieldmasker

A high-performance, type-safe implementation of response field masks in Rust,
inspired by Google APIs (FieldMask) and Google Maps API Field Mask
behavior.
Field masks let API clients request only the fields they need, which:
- Reduces response size and serialization cost.
- Avoids unnecessary CPU work (you can skip computing unneeded fields).
- Allows usage tracking per field.
- Provides strong validation: unknown fields are rejected.
Features
- Pure Rust core, no HTTP framework dependencies.
- Serde integration, mask filtering happens during serialization.
- Derive macro (
#[derive(MaskSpec)]) generates field mask metadata from your types.
- Axum integration (optional feature), extractors for
?fields= query or x-fields header.
- Strict validation, unknown field paths produce an error.
- Wildcard support (
* to select all fields at a level).
- Nested paths like
places.displayName.text are supported.
- Early-gating helpers (
contains_exact, intersects) to skip expensive computations.
Basic usage
[dependencies]
fieldmasker = "0.1"
use fieldmasker::{FieldMask, Masked, MaskSpec};
use serde::Serialize;
#[derive(Serialize, MaskSpec)]
#[serde(rename_all = "camelCase")]
struct DisplayName {
text: String,
language_code: String,
}
#[derive(Serialize, MaskSpec)]
#[serde(rename_all = "camelCase")]
struct Place {
id: String,
formatted_address: String,
display_name: DisplayName,
}
#[derive(Serialize, MaskSpec)]
struct SearchResponse {
places: Vec<Place>,
}
fn main() {
let data = SearchResponse {
places: vec![Place {
id: "abc".into(),
formatted_address: "1 High St".into(),
display_name: DisplayName {
text: "Foo".into(),
language_code: "en".into(),
},
}],
};
let mask = FieldMask::parse("places.formattedAddress,places.displayName.text").unwrap();
let masked = Masked::new(data, mask);
println!("{}", serde_json::to_string_pretty(&masked).unwrap());
}
Output:
{
"places": [
{
"formattedAddress": "1 High St",
"displayName": {
"text": "Foo"
}
}
]
}
Axum integration
Enable the axum feature to get request extractors that validate and parse masks:
use axum::{routing::get, Json, Router};
use fieldmasker::{Masked, MaskSpec};
use fieldmasker::axum_integration::{MaskRequired, MaskRejection};
use serde::Serialize;
#[derive(Serialize, MaskSpec)]
struct Item {
id: String,
name: String,
#[serde(skip)]
secret: String
}
#[derive(Serialize, MaskSpec)]
struct List {
items: Vec<Item>
}
async fn list(
MaskRequired::<List>(mask, _): MaskRequired<List>
) -> Result<Json<Masked<List>>, MaskRejection> {
let data = List {
items: vec![
Item { id: "1".into(), name: "A".into(), secret: "s1".into() },
Item { id: "2".into(), name: "B".into(), secret: "s2".into() },
],
};
Ok(Json(Masked::new(data, mask)))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/list", get(list));
axum::serve(tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(), app)
.await
.unwrap();
}
Example request:
$ curl 'http://localhost:3000/list?fields=items.name'
Response:
{
"items": [
{
"name": "A"
},
{
"name": "B"
}
]
}