json-masker: Zero-Dependency JSON Masking Library Claims 11x Speedup Over Jackson

This article introduces json-masker, a lightweight (~60 KB) zero-dependency Java library for masking sensitive fields in JSON, demonstrating up to 11x higher throughput than Jackson-based approaches with support for JSONPath, streaming API, allowlist/blocklist modes, and customizable masking strategies.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
json-masker: Zero-Dependency JSON Masking Library Claims 11x Speedup Over Jackson

Introduction

In Spring Boot projects, masking sensitive data in logs and API responses is a standard requirement. Traditional solutions often bind tightly to heavy serialization libraries like Jackson, adding bundle size and introducing performance bottlenecks under high concurrency due to reflection and temporary object allocation. The open-source library json-masker offers a different approach: a single-pass scanning core with zero external dependencies, designed for high-throughput masking in the Spring Boot ecosystem.

Feature Comparison

The article compares json-masker against two common alternatives: Jackson with manual handling and regular expressions.

Performance: json-masker ~454,000 ops/sec; Jackson + manual ~40,000 ops/sec; Regex ~5,000 ops/sec.

Convenient API: json-masker ✅; Jackson + manual ❌ (requires manual implementation); Regex ❌ (requires manual implementation).

Zero dependencies: json-masker ✅; Jackson + manual ❌; Regex ✅.

JSONPath support: json-masker ✅; Jackson + manual ⚠️ (requires extra library); Regex ❌.

Streaming API: json-masker ✅; Jackson + manual ✅; Regex ❌.

Preserves original formatting: json-masker ✅; Jackson + manual ❌; Regex ✅.

Size: json-masker ~60 KB; Jackson + manual ~2.5 MB+; Regex N/A.

Quick Start

Environment: Spring Boot 3.5.0

Maven Dependency

<dependency>
  <groupId>dev.blaauwendraad</groupId>
  <artifactId>json-masker</artifactId>
  <version>1.1.4</version>
</dependency>

Basic Usage

var jsonMasker = JsonMasker.getMasker(Set.of("email", "password"));
String masked = jsonMasker.mask("""
    {"name": "Pack", "email": "[email protected]", "password": "123123"}
    """);
System.err.println(masked);

Output:

{"name": "Pack", "email": "***", "password": "***"}

Note: Reuse the JsonMasker instance; it pre-processes keys for optimal performance.

JsonMasker Instance Creation

Several factory methods are available via JsonMaskingConfig.builder():

Blocklist mode (keys): JsonMasker.getMasker(Set.of("email", "iban")) — masks specified keys recursively at any nesting level.

Blocklist mode (JSONPath):

maskJsonPaths(Set.of("$.email", "$.nested.iban", "$.organization.*.name"))

— masks only at the exact paths; wildcards (*) supported for array elements.

Allowlist mode (keys): allowKeys(Set.of("id", "name")) — masks everything except the listed keys.

Allowlist mode (JSONPath):

allowJsonPaths(Set.of("$.id", "$.clients.*.phone", "$.nested.name"))

.

Key and JSONPath modes can be mixed. Simple keys apply recursively; JSONPath targets specific locations only.

Default Masking Behavior

With default configuration (block mode), the library replaces values with type-specific placeholders (e.g., strings become "***", numbers become 0, booleans become false). The article includes screenshots showing before/after JSON for a sample payload containing fields like email, age, visaApproved, iban, and billingAddress.

Allowlist (Whitelist) Example

var jsonMasker = JsonMasker.getMasker(
    JsonMaskingConfig.builder()
        .allowKeys(Set.of("orderId", "id", "travelPurpose", "successful"))
        .build());
String maskedJson = jsonMasker.mask(json);

Only the allowed keys remain visible; all others are masked.

Custom Mask Characters

Any type's default mask can be overridden:

var jsonMasker = JsonMasker.getMasker(
    JsonMaskingConfig.builder()
        .maskKeys(Set.of("email", "age", "visaApproved", "iban", "billingAddress"))
        .maskStringsWith("[redacted]")
        .maskNumbersWith("[redacted]")
        .maskBooleansWith("[redacted]")
        .build());

Streaming API

For large JSON payloads, a streaming API avoids loading the entire document into memory:

var jsonMasker = JsonMasker.getMasker(
    JsonMaskingConfig.builder()
        .maskKeys(Set.of("email", "iban"))
        .build());
jsonMasker.mask(jsonInputStream, jsonOutputStream);

All masking features work identically in streaming and in-memory modes.

JSONPath Details and Limitations

JSONPath enables precise control over nested structures. The following features are not supported :

Descendants (..)

Recursive descent

Name selectors

Array slice selectors

Index selectors

Filter selectors

Function extensions

Escape characters

Additional constraints:

Numeric keys are not allowed.

JSONPath keys must not contain ambiguous segments sharing the same path prefix (e.g., $.payment.iban and $.payment.*.address conflict at segment 2; $.payment.iban and $.customerDetails.*.address are allowed).

A JSONPath cannot end with a leading wildcard; use $.a instead of $.a.*.

JSONPath Example

var jsonMasker = JsonMasker.getMasker(
    JsonMaskingConfig.builder()
        .maskJsonPaths(Set.of(
            "$.customerDetails.email",
            "$.customerDetails.age",
            "$.customerDetails.visaApproved",
            "$.payment.iban",
            "$.payment.billingAddress",
            "$.customerDetails.identificationDocuments.*.number"
        ))
        .build());
String maskedJson = jsonMasker.mask(json);

The article includes a screenshot of the masked output for this configuration.

Length-Preserving Masking

To retain the original string length or digit count, use character-level masking:

var jsonMasker = JsonMasker.getMasker(
    JsonMaskingConfig.builder()
        .maskKeys(Set.of("email", "age", "visaApproved", "iban", "billingAddress"))
        .maskStringCharactersWith("*")
        .maskNumberDigitsWith(8)
        .build());
String maskedJson = jsonMasker.mask(json);

Strings are replaced with asterisks of the same length; numbers are replaced with the digit 8 repeated to match the original digit count. A screenshot illustrates the result.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performance benchmarkSpring BootJSONPathdata desensitizationstreaming APIzero-dependencyjson-maskerJSON masking
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.