0

I want to be able to conditionally use a custom JsonConverterAttribute which in turn creates a type of custom JsonConverter when calling JsonSerializer.Serialize()

My current implementation means that it works every time a string that has the attribute is called however i want to limit it to when a type of JsonConverter is passed into the JsonSeriliazationOptions

My current implementation looks like this:

Converter:

    public class SensitiveConverter : JsonConverter<string?>
    {
        public int MaskLength { get; }

        public SensitiveConverter(int maskLength = 4)
        {
            MaskLength = maskLength;
        }

        public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return reader.GetString();
        }

        public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
        {
            //do something with maskLength here
            writer.WriteStringValue(value);
        }
    }

Attribute:

    [AttributeUsage(AttributeTargets.Property)]
    public class SensitiveAttribute : JsonConverterAttribute
    {
        public int MaskLength { get; }

        public SensitiveAttribute(int maskLength = 0)
        {
            MaskLength = maskLength;
        }

        public override JsonConverter? CreateConverter(Type typeToConvert)
        {
            return new SensitiveConverter(MaskLength);
        }
    }

Example class using attribute:

    public class SampleClass
    {
        [Sensitive(4)]
        public string TheMaskedProperty { get; set; } = "mask me";

        public string TheUnMaskedProperty { get; set; } = "dont dare mask me";
    }

Given this implementation every time i call JsonSerializer.Serialize() it will use the converter on the attributed properties, however i would like control over this and for it to be only applied when i specify a parameter or flag in the JsonSerializerOptions. I have an implementation of this that works, however it feels more like a workaround rather than a proper implementation. Currently i am implementing it as follows:

Updated converter:

    public class SensitiveConverter : JsonConverter<string?>
    {
        public int MaskLength { get; }

        public SensitiveConverter(int maskLength = 4)
        {
            MaskLength = maskLength;
        }

        public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return reader.GetString();
        }

        public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
        {
            var exists = options.Converters.Any(e => e.GetType() == typeof(SensitiveFlag));

            if (!exists)
                return;

            //do something with maskLength here
            writer.WriteStringValue(value);
        }
    }

New converter that is essentially used as a flag and does nothing else:

    public class SensitiveFlag : JsonConverter<object>
    {
        public override bool CanConvert(Type typeToConvert) => false;


        public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException();


        public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) => throw new NotImplementedException();
    }

Example use

var sampleClassInstance = new SampleClass();
var generatedJson = JsonSerializer.Serialize(sampleClassInstance, new JsonSerializerOptions { Converters = { new SensitiveFlag() } });

This does work as i expect, however as mentioned it feels like a hack rather than a proper implementation. I have tried to use a JsonConverterFactory however that doesn't work for anything but strings as that's what my converter deals with. If i apply the required converter in the options directly, it works but will serialize for none or all.

So what is the correct way to implement this?

teimaj
  • 314
  • 5
  • 14

1 Answers1

1

The JsonSerializerOptions class is sealed, so you can not extend it. However you can create an extension method to enable/disable this custom option. Here is a code snip:

EDIT: Improving JsonSerializerOptionsExt now using a ConditionalWeakTable.

public static class JsonSerializerOptionsExt
{
    private static readonly ConditionalWeakTable<object, object?> CwtUseSensitive = new();
    
    public static JsonSerializerOptions UseSensitive(this JsonSerializerOptions options)
    {
        CwtUseSensitive.AddOrUpdate(options, null);
        return options;
    }

    public static bool HasSensitive(this JsonSerializerOptions options) =>
        CwtUseSensitive.TryGetValue(options, out _);
}

You can keep your attribute's implementation.

[AttributeUsage(AttributeTargets.Property)]
public class SensitiveAttribute : JsonConverterAttribute
{
    public int MaskLength { get; }

    public SensitiveAttribute(int maskLength = 0)
    {
        MaskLength = maskLength;
    }

    public override JsonConverter? CreateConverter(Type typeToConvert)
    {
        return new SensitiveConverter(MaskLength);
    }
}

And here is the SensitiveConverter:

public class SensitiveConverter : JsonConverter<string?>
{
    public int MaskLength { get; }

    public SensitiveConverter(int maskLength = 4)
    {
        MaskLength = maskLength;
    }

    public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return reader.GetString();
    }

    public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
    {
        if (options.IsSensitive())
        {
            //do something with MaskLength here
            //example: replace the char by _
            if (!string.IsNullOrWhiteSpace(value) && MaskLength > 0)
            {
                var sb = new StringBuilder(value);
                for (var i = 0; i < MaskLength && i < sb.Length; sb[i++] = '_') ;
                value = sb.ToString();
            }
        }

        writer.WriteStringValue(value);
    }
}

Here is a demo (some segments have been removed to make it shorter):

...
public class SampleClass
{
    [Sensitive(4)]
    public string TheMaskedProperty { get; set; } = "mask me";

    public string TheUnMaskedProperty { get; set; } = "dont dare mask me";
}

...
var p = new SampleClass();
var output = JsonSerializer.Serialize(p, new JsonSerializerOptions().UseSensitive());
...

The output is: {"TheMaskedProperty":"____ me","TheUnMaskedProperty":"dont dare mask me"}

denys-vega
  • 3,522
  • 1
  • 19
  • 24