Mask Sensitive Data in Java with YAML Rules – A Step‑by‑Step Guide
This article demonstrates a practical approach to data desensitization in Java applications by storing masking rules in YAML files, loading them into maps, and recursively applying regex‑based transformations to nested response structures without using AOP annotations, complete with sample code and execution results.
Introduction
When a project requires masking specific fields in a transaction interface response without using AOP annotations, a straightforward map‑based solution can be applied.
Masking Process Overview
Store all fields that need masking and their corresponding rules in a Map.
When the interface returns, iterate over all fields and check if the field name exists in the map.
Return the masked result.
Understanding YAML Syntax
YAML (YAML Ain’t Markup Language) is a lightweight data‑serialization format designed for human readability. Compared with JSON, XML, and Properties files, YAML offers more concise syntax, supports complex data structures, and is easily parsed across many programming languages.
Basic Syntax
Indentation represents hierarchy (spaces or tabs, but not mixed).
Use a colon : for key‑value pairs.
Use a dash - for list items.
# Using indentation to represent hierarchy
server:
port: 8080
# Key‑value pair
name: John Smith
age: 30
# List items
hobbies:
- reading
- hiking
- swimmingComments
Comments start with # and can appear at the beginning or end of a line.
# This is a comment
name: John Smith # Inline commentStrings
Strings may be quoted with single or double quotes, or left unquoted.
Double‑quoted strings support escape sequences like \n and \u.
# Double‑quoted string
name: "John Smith"
# Single‑quoted string
nickname: 'Johnny'Key‑Value Pairs and Lists
Keys and values are separated by a colon and a space.
Values can be scalars, strings, lists, or nested maps.
# Nested map example
address:
city: San Francisco
state: California
zip: 94107Advanced Features
Anchors ( &) and aliases ( *) allow reuse of nodes.
Multi‑line strings can be written with | (preserve newlines) or > (fold newlines).
# Preserve newlines
description: |
This is a
multi‑line string.
# Folded string
summary: >
This is a summary that may contain
line breaks.Defining Masking Rule Formats
For simple structures, the rule format is TransactionID->Field->Rule. For nested lists, use TransactionID->Field(List)->SubField->Rule. These hierarchical keys enable direct retrieval of masking rules from a Map.
TransactionID:
FieldName:
rule: '/^(1[3-9][0-9])\d{4}(\d{4}$)/'Loading YAML Configuration
Create a desensitize.yml file with rules, then load it into a Map using SnakeYAML.
public static Map<String, Object> loadYaml(String yamlFile) {
Yaml yaml = new Yaml();
try (InputStream in = DataDesensitizationUtils.class.getResourceAsStream(yamlFile)) {
return yaml.loadAs(in, Map.class);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}Recursive Retrieval of Nested Rules
public static Map<String, Object> getNestedMapValues(Map<String, Object> map, String... keys) {
if (keys.length == 0 || !map.containsKey(keys[0])) {
return null;
}
Object nested = map.get(keys[0]);
if (keys.length == 1) {
return (nested instanceof Map) ? (Map<String, Object>) nested : null;
}
if (nested instanceof Map) {
return getNestedMapValues((Map<String, Object>) nested, Arrays.copyOfRange(keys, 1, keys.length));
}
return null;
}Masking Logic
private static String desensitizeLogic(String data, Map<String, Object> map) {
if (map.containsKey("rule")) {
String rule = (String) map.get("rule");
String sign = map.getOrDefault("format", "*").toString();
return data.replaceAll(rule, sign);
}
return data;
}Recursive Data Traversal and Masking
public static void parseData(Object entity, String servNo, String path) {
if (entity instanceof Map) {
for (Map.Entry<String, Object> entry : ((Map<String, Object>) entity).entrySet()) {
String currentPath = path.isEmpty() ? entry.getKey() : path + "," + entry.getKey();
if (entry.getValue() instanceof Map) {
parseData(entry.getValue(), servNo, currentPath);
} else if (entry.getValue() instanceof List) {
for (Object item : (List) entry.getValue()) {
if (item instanceof Map) {
parseData(item, servNo, currentPath);
}
}
} else {
String[] keyPaths = (servNo + "," + currentPath).split(",");
Map<String, Object> ruleMap = getNestedMap(keyPaths);
if (ruleMap != null) {
String masked = desensitizeLogic(entry.getValue().toString(), ruleMap);
entry.setValue(masked);
}
}
}
}
}Testing the Utility
A sample Demo class builds a nested data structure, loads desensitize.yml, and invokes parseData. Console output shows original and masked values, confirming the approach works for both simple fields and list items.
public class Demo {
public static void main(String[] args) {
Map<String, Object> data = getData();
if (data.containsKey("txHeader") && data.get("txHeader") instanceof Map) {
String servNo = ((Map) data.get("txHeader")).get("servNo").toString();
DataDesensitizationUtils.parseData(data.get("txEntity"), servNo, "");
}
}
// getData() builds sample payload (omitted for brevity)
}Complete Utility Class
/**
* DataDesensitizationUtils – Utility for masking sensitive data using YAML rules.
*/
@Slf4j
@SuppressWarnings("unchecked")
public class DataDesensitizationUtils {
private static final String YAML_FILE_PATH = "/tuomin.yml";
private static Map<String, Object> map;
static {
Yaml yaml = new Yaml();
try (InputStream in = DataDesensitizationUtils.class.getResourceAsStream(YAML_FILE_PATH)) {
map = yaml.loadAs(in, Map.class);
} catch (Exception e) {
e.printStackTrace();
}
}
private static Map<String, Object> getNestedMap(String... keys) {
return getNestedMapValues(map, keys);
}
private static Map<String, Object> getNestedMapValues(Map<String, Object> map, String... keys) {
if (keys.length == 0 || !map.containsKey(keys[0])) return null;
Object nested = map.get(keys[0]);
if (keys.length == 1) return (nested instanceof Map) ? (Map<String, Object>) nested : null;
if (nested instanceof Map) return getNestedMapValues((Map<String, Object>) nested, Arrays.copyOfRange(keys, 1, keys.length));
return null;
}
public static void parseData(Object entity, String servNo, String path) {
if (entity instanceof Map) {
for (Map.Entry<String, Object> entry : ((Map<String, Object>) entity).entrySet()) {
String currentPath = path.isEmpty() ? entry.getKey() : path + "," + entry.getKey();
if (entry.getValue() instanceof Map) {
parseData(entry.getValue(), servNo, currentPath);
} else if (entry.getValue() instanceof List) {
for (Object item : (List) entry.getValue()) {
if (item instanceof Map) parseData(item, servNo, currentPath);
}
} else {
String[] keyPaths = (servNo + "," + currentPath).split(",");
Map<String, Object> ruleMap = getNestedMap(keyPaths);
if (ruleMap != null) {
String masked = desensitizeLogic(entry.getValue().toString(), ruleMap);
entry.setValue(masked);
}
}
}
}
}
private static String desensitizeLogic(String data, Map<String, Object> map) {
if (map.containsKey("rule")) {
String rule = (String) map.get("rule");
String sign = map.getOrDefault("format", "*").toString();
return data.replaceAll(rule, sign);
}
return data;
}
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
