3

I have an enum:

public enum TaxType : byte
{
    None = 0,
    GSTCanada = 5,
    HSTOntario = 13,
    HSTOther = 15
}

It is given in the json by a number. Eg:

{"TaxType": 13, ...}
public class OrderInfo
{
    public TaxType TaxType { get; set; }
    /// ...
}

It will successfully deserialize but I want to throw an exception if the value is NOT (0 OR 5 OR 13 OR 15).

Is that possible to do via an attribute using System.Text.Json?

trinalbadger587
  • 1,905
  • 1
  • 18
  • 36
  • What happens when you pass e.g. `{"TaxType": 14, ...}` – Roar S. Jul 15 '21 at 16:50
  • It should throw an Exception but it doesn't: https://dotnetfiddle.net/FBzih3 – trinalbadger587 Jul 15 '21 at 16:53
  • It is strange how c# will allow invalid values of `enum`s but not of other types like `bool`. – trinalbadger587 Jul 15 '21 at 17:17
  • 1
    Both System.Text.Json.JsonSerializer.Deserialize and JsonConvert.DeserializeObject deserializes values not present in enum. This code runs as well: `TaxType taxType = (TaxType) 22;` This is described here way back in time: https://stackoverflow.com/a/618312/14072498 Hence, to solve this issue, someone needs to dig up a custom attribute. – Roar S. Jul 15 '21 at 17:23
  • It's not strange, it's the language definition. And nothing stops you from [setting a `bool` to `4`](http://share.linqpad.net/9fs7xt.linq) if you want! – Blindy Jul 15 '21 at 17:53
  • @Blindy, I can see that if you try and find a loophole in the language it it possible but almost all the time a bool is either true or false. Eg. if you do the obvious thing: `(bool)4`, it doesn't work. – trinalbadger587 Jul 15 '21 at 21:29

2 Answers2

4

Improvement on @MestreDosMagros's answer:

I created a [CheckedEnum] attribute.

It will throw an exception if the value is not in the enum.

You will need a different algorithm for a [Flags] enum.

Example:
public class OrderInfo
{
    public TaxType TaxType { get; set; }
}

[CheckedEnum]
public enum TaxType : byte
{
    None = 0,
    GSTCanada = 5,
    HSTOntario = 13,
    HSTOther = 15
}

(.NET fiddle tests)

Code:

public class CheckedEnumAttribute : JsonConverterAttribute { public CheckedEnumAttribute() : base(typeof(CheckedEnumConverterFactory)) { } }

public class CheckedEnumConverterFactory : JsonConverterFactory
{
    public override bool CanConvert(Type typeToConvert)
        => typeToConvert.IsEnum;

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
        => (JsonConverter)Activator.CreateInstance(typeof(CheckedEnumConverter<>).MakeGenericType(typeToConvert));
}

public class CheckedEnumConverter<T> : JsonConverter<T> where T: struct, Enum
{
    static readonly TypeCode typeCode = Type.GetTypeCode(typeof(T));

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        T value = (T)(object)(typeCode switch
        {
            TypeCode.SByte => reader.GetSByte(),
            TypeCode.Byte => reader.GetByte(),
            TypeCode.Int16 => reader.GetInt16(),
            TypeCode.UInt16 => reader.GetUInt16(),
            TypeCode.Int32 => reader.GetInt32(),
            TypeCode.UInt32 => reader.GetUInt32(),
            TypeCode.Int64 => reader.GetInt64(),
            TypeCode.UInt64 => reader.GetUInt64()
        });
        if (!Enum.IsDefined(value)) throw new Exception($"Value {value} is invalid!");
        return value;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        if (!Enum.IsDefined(value)) throw new Exception($"Value {value} is invalid!");
        if (typeCode == TypeCode.UInt64)
            writer.WriteNumberValue((ulong)(object)value);
        else
            writer.WriteNumberValue(Convert.ToInt64(value));
    }
}
trinalbadger587
  • 1,905
  • 1
  • 18
  • 36
2

You can create a custom converter for your enum, like this:

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

var jsonOpt = new JsonSerializerOptions();
jsonOpt.Converters.Add(new CustomEnumConverter());
Console.WriteLine(JsonSerializer.Deserialize<OrderInfo>("{\"TaxType\":14}", jsonOpt).TaxType);

public enum TaxType
{
    None = 0,
    GSTCanada = 5,
    HSTOntario = 13,
    HSTOther = 15
}

public class OrderInfo
{
    public TaxType TaxType { get; set; }
}

public class CustomEnumConverter : JsonConverter<TaxType>
{
    public override TaxType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var value = reader.GetInt32();
        if (Enum.IsDefined(typeToConvert, value))
        {
            return (TaxType) value;
        }
        throw new Exception("Value is invalid");
    }

    public override void Write(Utf8JsonWriter writer, TaxType value, JsonSerializerOptions options)
    {
        writer.WriteNumber("taxType", (decimal)(int)value);
    }
}

Obviously this is not totally generic and you will need to do some work, but you get the idea.

MestreDosMagros
  • 1,000
  • 5
  • 19