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.

Architect
Architect
Architect
Mask Sensitive Data in Java with YAML Rules – A Step‑by‑Step Guide

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
  - swimming

Comments

Comments start with # and can appear at the beginning or end of a line.

# This is a comment
name: John Smith  # Inline comment

Strings

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: 94107

Advanced 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;
    }
}
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.

Javadata maskingdesensitization
Architect
Written by

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.

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.