Patola
Patola

Reputation: 673

Convert JsonConverter to System.Text.Json to support multiple primitive types and nullable

I am trying to convert this Newtonsoft.Json.JsonConverter to System.Text.Json. However, I was only able to use a single primitive type, say double and even there I cant apply the converter on nullable (double?). How can I convert this to support nullable and all number formats (float, double).

Newtonsoft.Json

public class DecimalRoundingJsonConverter : JsonConverter
{
    private readonly int _numberOfDecimals;

    public DecimalRoundingJsonConverter() : this(6)
    {
    }
    public DecimalRoundingJsonConverter(int numberOfDecimals)
    {
        _numberOfDecimals = numberOfDecimals;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        double input = 0;
        if (value is decimal)
        {
            var d = (decimal)value;
            input = Convert.ToDouble(d);
        }
        else if (value is float)
        {
            var d = (float)value;
            input = Convert.ToDouble(d);
        }
        else
        {
            input = (double)value;
        }
        var rounded = Math.Round(input, _numberOfDecimals);
        writer.WriteValue((decimal)rounded);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanRead
    {
        get { return false; }
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(decimal);
    }
}

System.Text.Json (basic)

public class DecimalRoundingJsonConverter : JsonConverter<double>
{
    private readonly int _numberOfDecimals = 6;
    public override double Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(
        Utf8JsonWriter writer,
        double dvalue,
        JsonSerializerOptions options)
    {
        double input = (double)dvalue;
      
        var rounded = Math.Round(input, _numberOfDecimals);
        writer.WriteStringValue(rounded.ToString());
    }
}

Upvotes: 3

Views: 3566

Answers (1)

dbc
dbc

Reputation: 116795

You can create a converter that applies to all float, double and decimal values, as well as nullables of the same, by creating a JsonConverter<object> and overriding JsonConverter<object>.CanConvert(Type) to return true only for the six relevant types.

The following does the job:

public class RoundingJsonConverter : RoundingJsonConverterBase
{
    // The converter works for float, double & decimal.  Max number of decimals for double is 15, for decimal is 28, so throw an exception of numberOfDecimals > 28.
    public RoundingJsonConverter(int numberOfDecimals) => NumberOfDecimals = (numberOfDecimals < 0 || numberOfDecimals > 28 ? throw new ArgumentOutOfRangeException(nameof(numberOfDecimals)) : numberOfDecimals);
    protected override int NumberOfDecimals { get; }
}

public class RoundingTo2DigitsJsonConverter : RoundingJsonConverterBase
{
    protected override int NumberOfDecimals { get; } = 2;
}

public class RoundingTo6DigitsJsonConverter : RoundingJsonConverterBase
{
    protected override int NumberOfDecimals { get; } = 6;
}

public abstract class RoundingJsonConverterBase : JsonConverter<object>
{
    protected abstract int NumberOfDecimals { get; }

    public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        typeToConvert = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
        if (typeToConvert == typeof(decimal))
            return reader.GetDecimal();
        else if (typeToConvert == typeof(double))
            return reader.GetDouble();
        else if (typeToConvert == typeof(float))
            return (float)reader.GetDouble();
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        switch (value)
        {
            case double d:
                writer.WriteNumberValue(Math.Round(d, Math.Min(15, NumberOfDecimals)));
            break;
            case decimal d:
                writer.WriteNumberValue(Math.Round(d, NumberOfDecimals));
            break;
            case float f:
                writer.WriteNumberValue((decimal)Math.Round((decimal)f, NumberOfDecimals));
            break;
            default:
                throw new NotImplementedException();
        }
    }
    
    public override bool CanConvert(Type typeToConvert)
    {
        typeToConvert = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
        return typeToConvert == typeof(double) || typeToConvert == typeof(decimal) || typeToConvert == typeof(float);
    }           
}

Notes:

  • System.Text.Json has no equivalent to Newtonsoft's JsonConverter.CanRead so you must implement Read() as well as Write().

  • When adding the converter to JsonSerializerOptions.Converters use DecimalRoundingJsonConverter and pass the required number of digits as a constructor argument, e.g.:

    var options = new JsonSerializerOptions
    {
        Converters = { new RoundingJsonConverter(6) },
    };
    

    However, if you are applying the converter via attributes, Microsoft does not allow passing of converter parameters (see here for confirmation) so you will need to create a specific converter type for each required number of digits, e.g.

    public class RoundingTo2DigitsJsonConverter : RoundingJsonConverterBase
    {
        protected override int NumberOfDecimals { get; } = 2;
    }
    
    public class RoundingTo6DigitsJsonConverter : RoundingJsonConverterBase
    {
        protected override int NumberOfDecimals { get; } = 6;
    }
    

    And then apply e.g. as follows:

    [JsonConverter(typeof(RoundingTo6DigitsJsonConverter))]
    public decimal? SixDecimalPlaceValue { get; set; }
    
  • Nullable.GetUnderlyingType(Type) can be use used to get the underlying type of a nullable type such as decimal? or double?.

  • JsonConverter<T>.Write() is never called for null values of nullables unless JsonConverter<T>.HandleNull is overridden to return true.

Demo fiddle here.

Upvotes: 3

Related Questions