fieldmasker

Crates.iofieldmasker
lib.rsfieldmasker
version0.0.1
created_at2025-08-26 11:35:59.237366+00
updated_at2025-08-26 11:35:59.237366+00
descriptionA utility for selecting and filtering response fields via field masks.
homepagehttps://github.com/punkeel/fieldmasker
repositoryhttps://github.com/punkeel/fieldmasker
max_upload_size
id1811049
size98,269
Maxime Guerreiro (punkeel)

documentation

https://docs.rs/fieldmasker

README

fieldmasker

Crates.io Docs.rs License

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"

# For Axum extractors:
# fieldmasker = { version = "0.1", features = ["axum"] }
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"
    }
  ]
}
Commit count: 2

cargo fmt