DEV Community

Cover image for C# System.Text.Json JsonConverter masking
Karen Payne
Karen Payne

Posted on

C# System.Text.Json JsonConverter masking

Introduction

Learn how to use custom JsonConverters to export data which mask social security numbers and credit card numbers. These converters are useful when for instance in the case an extract is requested for a customer, but the asker does not have permissions to see either SSN or credit card numbers.

In both cases the entire value is not masked. This way if a verification is needed other than an export an employee may use the unmasked values for verification like in customer verification process.

For SQL-Server, consider using dynamic data masking is also an option which requires using database table permissions.

Note
An important aspect of the sample code is separation of converters from the frontend as the converters can be used in any project type. For novice developers when writing any code that may be used in other projects consider separating the code to reside in a class project and one step farther is to create a local NuGet package for the class project.

Converter source code front-end source code

Credit card code

For credit cards, the following display the last four numbers. Validation should be done prior to using this extension, for instance in a custom input or prior to a post operation.

namespace JsonConvertersSampleLibrary.Extensions;
public static partial class StringExtensions
{

    /// <summary>
    /// Masks a credit card number by replacing all but the last four digits with a specified mask character.
    /// </summary>
    /// <param name="sender">The credit card number to be masked.</param>
    /// <param name="maskCharacter">The character to use for masking the credit card number. Default is 'X'.</param>
    /// <returns>A masked version of the credit card number, with all but the last four digits replaced by the mask character.</returns>
    /// <remarks>
    /// The method uses a regular expression to identify and mask the credit card number.
    /// </remarks>
    public static string MaskCreditCardNumber(this string sender, char maskCharacter = 'X')
    {

        if (string.IsNullOrEmpty(sender))
        {
            return sender;
        }

        return CreditCardMaskRegEx().Replace(sender, match =>
        {
            var digits = string.Concat(match.Value.Where(char.IsDigit));

            return digits.Length is 16 or 15
                ? new string(maskCharacter, digits.Length - 4) + digits[^4..]
                : match.Value;
        });
    }

    [GeneratedRegex("[0-9][0-9 ]{13,}[0-9]")]
    private static partial Regex CreditCardMaskRegEx();
}
Enter fullscreen mode Exit fullscreen mode

Then create the custom JsonConverter<string> which for the read method returns the credit card value while the write operation uses MaskSsn extension method to mask the credit card value.

using System.Text.Json;
using System.Text.Json.Serialization;
using JsonConvertersSampleLibrary.Extensions;

namespace JsonConvertersSampleLibrary.Converters;
/// <summary>
/// Provides a custom JSON converter for Social Security Numbers (SSNs) that masks the SSN value during serialization.
/// </summary>
public class SocialSecurityMaskConverter : JsonConverter<string>
{
    /// <summary>
    /// Reads and converts the JSON to a string value.
    /// </summary>
    /// <param name="reader">The <see cref="Utf8JsonReader"/> to read from.</param>
    /// <param name="typeToConvert">The type of the object to convert.</param>
    /// <param name="options">Options to control the behavior during reading.</param>
    /// <returns>The string value read from the JSON.</returns>
    public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 
        => reader.GetString()!;

    /// <summary>
    /// Writes a masked Social Security Number (SSN) to the JSON.
    /// </summary>
    /// <param name="writer">The <see cref="Utf8JsonWriter"/> to write to.</param>
    /// <param name="value">The SSN value to be masked and written.</param>
    /// <param name="options">Options to control the behavior during writing.</param>
    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.MaskSsn());
    }
}
Enter fullscreen mode Exit fullscreen mode

To use the converter the following class/model assigns the convert to the SSN property which will be used when serializing data.

using System.Text.Json.Serialization;
using JsonConvertersSampleLibrary.Converters;

namespace JsonConvertersSampleApp.Models;

/// <summary>
/// Represents a customer with personal details and sensitive information such as credit card number and PIN.
/// Inherits from the <see cref="Person"/> class.
/// </summary>
public class Customer : Person
{
    /// <summary>
    /// Gets or sets the credit card number of the customer.
    /// The credit card number is masked when serialized to JSON using the <see cref="CreditCardMaskConverter"/>.
    /// </summary>
    [JsonConverter(typeof(CreditCardMaskConverter))]
    public string CreditCardNumber { get; set; }
    public string PIN { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

To serialize, in this case a list of Taxpayer, mocked data is generated using NuGet package Bogus.

Code for json options

private static JsonSerializerOptions Options =>
    new()
    {
        WriteIndented = true
    };
Enter fullscreen mode Exit fullscreen mode

Serialize list to a json string.

var json = JsonSerializer.Serialize(BogusOperations.TaxpayerList(), Options);
Enter fullscreen mode Exit fullscreen mode

Since the json converter is not tied to a single type but a string property it can be used with other classes as shown below.

/// <summary>
/// Represents a customer with personal details and sensitive information such as credit card number and PIN.
/// Inherits from the <see cref="Person"/> class.
/// </summary>
public class Customer : Person
{
    /// <summary>
    /// Gets or sets the credit card number of the customer.
    /// The credit card number is masked when serialized to JSON using the <see cref="CreditCardMaskConverter"/>.
    /// </summary>
    [JsonConverter(typeof(CreditCardMaskConverter))]
    public string CreditCardNumber { get; set; }
    public string PIN { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Sample serializing, this time without Bogus.

public static void ForCustomers()
{
    var customer1 = new Customer
    {
        Id = 1,
        FirstName = "John",
        LastName = "Jones",
        Title = "Mr.",
        CreditCardNumber = "123456 7890 123456",
        PIN = "5555",
        BirthDate = new DateOnly(1980, 1, 1),
    };

    var customer2 = new Customer
    {
        Id = 2,
        FirstName = "Mary",
        LastName = "Gallagher",
        Title = "Miss.",
        CreditCardNumber = "123 456 789 012 3456",
        PIN = "1234",
        BirthDate = new DateOnly(1980, 1, 1),
    };

    List<Customer> customers = [customer1, customer2];
    var json = JsonSerializer.Serialize(customers, Options);

    File.WriteAllText("Customers.json", json);

}
Enter fullscreen mode Exit fullscreen mode

Results

[
  {
    "CreditCardNumber": "XXXXXXXXXXXX3456",
    "PIN": "5555",
    "Id": 1,
    "FirstName": "John",
    "LastName": "Jones",
    "Title": "Mr.",
    "BirthDate": "1980-01-01",
    "Gender": "Male"
  },
  {
    "CreditCardNumber": "XXXXXXXXXXXX3456",
    "PIN": "1234",
    "Id": 2,
    "FirstName": "Mary",
    "LastName": "Gallagher",
    "Title": "Miss.",
    "BirthDate": "1980-01-01",
    "Gender": "Male"
  }
]
Enter fullscreen mode Exit fullscreen mode

SSN (Social Security number) code

The same pattern is used as with a credit card mask, only difference is the language extension method used.

The following will be used in the custom SSN JsonConverter<string>

#pragma warning disable CA1847
using JsonConvertersSampleLibrary.Classes;
using System.Text.RegularExpressions;

namespace JsonConvertersSampleLibrary.Extensions;
public static partial class StringExtensions
{

    /// <summary>
    /// Masks a Social Security Number (SSN) by replacing a specified number of digits with a mask character.
    /// </summary>
    /// <param name="ssn">The Social Security Number to be masked.</param>
    /// <param name="digitsToShow">The number of digits to show at the end of the SSN. Default is 4.</param>
    /// <param name="maskCharacter">The character to use for masking the SSN. Default is 'X'.</param>
    /// <returns>A masked version of the SSN.</returns>
    /// <exception cref="ArgumentException">Thrown when the SSN is invalid, either due to incorrect length or non-numeric characters.</exception>
    public static string MaskSsn(this string ssn, int digitsToShow = 4, char maskCharacter = 'X')
    {
        if (string.IsNullOrWhiteSpace(ssn)) return string.Empty;
        if (ssn.Contains("-")) ssn = ssn.Replace("-", string.Empty);
        if (ssn.Length != 9) throw new ArgumentException("SSN invalid length");
        if (ssn.IsNotInteger()) throw new ArgumentException("SSN not valid");

        const int ssnLength = 9;
        const string separator = "-";

        int maskLength = ssnLength - digitsToShow;
        int output = int.Parse(ssn.Replace(separator, string.Empty).Substring(maskLength, digitsToShow));

        var format = string.Empty;
        for (var index = 0; index < maskLength; index++)
        {
            format += maskCharacter;
        }

        for (var index = 0; index < digitsToShow; index++)
        {
            format += "0";
        }

        format = format.Insert(3, separator).Insert(6, separator);
        format = $"{{0:{format}}}";

        return string.Format(format, output);

    }

}
Enter fullscreen mode Exit fullscreen mode

Sample class

using System.Text.Json.Serialization;
using JsonConvertersSampleApp.Interfaces;
using JsonConvertersSampleLibrary.Converters;
using JsonConvertersSampleLibrary.Extensions;

namespace JsonConvertersSampleApp.Models;

/// <summary>
/// Represents a taxpayer with properties for Social Security Number (SSN), Personal Identification Number (PIN),
/// start date, and Employer Identification Number (EIN). Inherits from <see cref="Person"/> and implements <see cref="ITaxpayer"/>.
/// </summary>
public  class Taxpayer : Person, ITaxpayer
{
    /// <summary>
    /// Gets or sets the Social Security Number (SSN) of the taxpayer.
    /// The SSN is masked during JSON serialization using the <see cref="SocialSecurityMaskConverter"/>.
    /// </summary>
    [JsonConverter(typeof(SocialSecurityMaskConverter))]
    public string SSN { get; set; }

    public string PIN { get; set; }
    public DateOnly StartDate { get; set; }
    public string EmployerIdentificationNumber { get; set; }
    public override string ToString() => SSN.MaskSsn();
}
Enter fullscreen mode Exit fullscreen mode

Results

[
  {
    "SSN": "XXX-XX-9617",
    "PIN": "9685",
    "StartDate": "2004-07-12",
    "EmployerIdentificationNumber": "86-4138704",
    "Id": 1,
    "FirstName": "Clara",
    "LastName": "Reilly",
    "Title": "Chief Functionality Assistant",
    "BirthDate": "1919-04-22",
    "Gender": "Female"
  },
  {
    "SSN": "XXX-XX-7173",
    "PIN": "1396",
    "StartDate": "2002-08-27",
    "EmployerIdentificationNumber": "31-5100314",
    "Id": 2,
    "FirstName": "Tonya",
    "LastName": "Prosacco",
    "Title": "Lead Quality Planner",
    "BirthDate": "1927-08-27",
    "Gender": "Female"
  },
  {
    "SSN": "XXX-XX-1763",
    "PIN": "8868",
    "StartDate": "2008-12-29",
    "EmployerIdentificationNumber": "18-3837008",
    "Id": 3,
    "FirstName": "Jimmie",
    "LastName": "D\u0027Amore",
    "Title": "Corporate Program Strategist",
    "BirthDate": "1933-08-02",
    "Gender": "Male"
  },
  {
    "SSN": "XXX-XX-0774",
    "PIN": "0627",
    "StartDate": "2002-02-02",
    "EmployerIdentificationNumber": "45-0134382",
    "Id": 4,
    "FirstName": "Tracy",
    "LastName": "O\u0027Kon",
    "Title": "Internal Program Associate",
    "BirthDate": "1971-01-13",
    "Gender": "Male"
  },
  {
    "SSN": "XXX-XX-2705",
    "PIN": "6617",
    "StartDate": "2008-01-05",
    "EmployerIdentificationNumber": "60-2434511",
    "Id": 5,
    "FirstName": "Craig",
    "LastName": "Hauck",
    "Title": "Global Operations Assistant",
    "BirthDate": "1982-07-26",
    "Gender": "Male"
  }
]
Enter fullscreen mode Exit fullscreen mode

Summary

Two custom JsonConverters for use with string properties have been shown which when coupled with the following article provide an arsenal of converters to use or learn from to create other converters.

Top comments (0)