Newtonsoft.Json Ignores TypeConverters and/or JsonConverters, regardless of how it they're referenced

倖福魔咒の 提交于 2021-01-28 10:54:28

问题


I have an enum, for example:

[JsonConverter(typeof(MyJSONConverter))]
public enum MyEnum
{
    a,
    b,
    c
}

I have put the JsonConverter attribute on it.

I have defined the converter class:

public class MyJSONConverter : JsonConverter<MyEnum>
{
    public MyJSONConverter()
    {
    }

    public override void WriteJson(JsonWriter writer, MyEnum value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }

    public override MyEnum ReadJson(JsonReader reader, Type objectType, MyEnum existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        switch (reader.Value)
        {
            case "a":
                return MyEnum.a;
            case "b":
                return MyEnum.b;
            case "c":
                return MyEnum.c;
        }

        return serializer.Deserialize<MyEnum>(reader);
    }

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

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

Breakpoints dictate the class is being instantiated. But it never calls any of the member methods or properties.

To my knowledge, that's all it should take to work. It does not work.

I have tried TypeConverter and other classes that inherit JsonConverter, including the one without a generic argument, and the StringEnumConverter. None of them work.

Although seemingly not necessary in any examples I have read, I have tried adding it as a converter to my settings:

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.TypeNameHandling = TypeNameHandling.Auto;
        settings.ObjectCreationHandling = ObjectCreationHandling.Replace;
        settings.ContractResolver = new CShouldSerializeContractResolver();
        settings.Converters = new JsonConverter[] { new MyJSONConverter() };

        JsonSerializer = JsonSerializer.Create(settings);

If I manually add it to my converters, the converter class gets instantiated once, upon instantiating the JsonSerializer itself, and upon attempting to deserialize. It seems to never use any instances that are created.

The error clearly states to attempt to use a TypeConverter, but none of them work.

Could not convert string 'blahblah' to dictionary key type 'MyEnum'. 
Create a TypeConverter to convert from the string to the key type object. Path 
'Array[0].Keys.blahblah', line 1, position 1388.

Complete fiddle here, with exception occurring: https://dotnetfiddle.net/itCbER

Why doesn't Newtonsoft use any of the documented methods of converting?


回答1:


I've used a TypeSafeEnum with JsonConverter to control how values are transposed between my code and an external vendor API.

First, we setup our abstract TypeSafeEnumBase class that will be used by multiple objects.

// define a base class for our type safe enum
public abstract class TypeSafeEnumBase
{
    // the types for Name and Value could be different if needed...
    protected readonly string Name;  // would be string value on typical enum
    protected readonly int Value; // would be int value on typical enum

    protected TypeSafeEnumBase(int value, string name)
    {
        this.Name = name;
        this.Value = value;
    }

    public override string ToString()
    {
        return Name;
    }
}

Next, we implement our TypeSafeEnums that will inherit from the base enum. I've implemented two in this example, A class for MyEnum from your example and MemberLanguage, both inherit from the TypeSafeEnumBase. There is no actual enum being used, e.g. public enum MyEnum {}. I've added MemberLanguage to help show the benefit of the TypeSafeEnumConversion class that's used in the next step.

// define TypeSafeEnumBase that's a sealed class and inherits from the TypeSafeEnumBase
public sealed class MyEnum : TypeSafeEnumBase
{        
    // define 'enums'
    public static readonly MyEnum a = new MyEnum(1, "a");
    public static readonly MyEnum b = new MyEnum(2, "b");
    public static readonly MyEnum c = new MyEnum(3, "c");
    // add additional 'enums'...

    private MyEnum(int value, string name) :
        base(value, name)
    {
    }

    public static IEnumerable<MyEnum> MyEnums()
    {
        // this method only needed if you have to iterate over enums
        // and will need to be maintained as new MyEnums are added
        yield return a;
        yield return b;
        yield return c;
    }

    public static bool TryParse(int value, out MyEnum language)
    {
        return TryParse(value.ToString(), out language);
    }

    public static bool TryParse(string value, out MyEnum language)
    {
        try
        {
            language = Parse(value);
            return true;
        }
        catch
        {
            language = null;
            return false;
        }
    }

    public static MyEnum Parse(int value)
    {
        return Parse(value.ToString());
    }

    public static MyEnum Parse(string value)
    {
        switch (value)
        {
            case nameof(a):
            case "1":
                return MyEnum.a;
            case nameof(b):
            case "2":
                return MyEnum.b;
            case nameof(c):
            case "3":
                return MyEnum.c;
            default:
                return null;
                // throw new ArgumentOutOfRangeException(nameof(value), $"Unable to parse for value, '{value}'. Not found.");
        }
    }
}
public sealed class MemberLanguage : TypeSafeEnumBase
{
    // define 'enums'
    public static readonly MemberLanguage English = new MemberLanguage(1, "English");
    public static readonly MemberLanguage Spanish = new MemberLanguage(2, "Spanish");
    // add additional language types...
    // public static readonly MemberLanguage Farsi = new MemberLanguage(3, "Farsi");
    // etc...

    private MemberLanguage(int value, string name) :
        base(value, name)
    {
    }

    public static bool TryParse(int value, out MemberLanguage language)
    {
        return TryParse(value.ToString(), out language);
    }

    public static bool TryParse(string value, out MemberLanguage language)
    {
        try
        {
            language = Parse(value);
            return true;
        }
        catch
        {
            language = null;
            return false;
        }
    }

    public static MemberLanguage Parse(int value)
    {
        return Parse(value.ToString());
    }

    public static MemberLanguage Parse(string value)
    {
        switch (value)
        {
            case nameof(English):
            case "ENG":
            case "1":
                return MemberLanguage.English;
            case nameof(Spanish):
            case "SPN":
            case "2":
                return MemberLanguage.Spanish;
            default:
                return null;
                // throw new ArgumentOutOfRangeException(nameof(value), $"Unable to parse for value, '{value}'. Not found.");
        }
    }
}

Next, we create a conversion helper for our TypeSafeEnums. By creating this helper we can ensure our JsonConverter will not need to change should we add additional type safe enums. Should we add more, this class will get updated but the JsonConverter will not need to be updated.

public class TypeSafeEnumConversion
{
    public static object ConvertToTypeSafeEnum(string name, string value)
    {
        object returnValue = null;
        switch (name)
        {
            case nameof(MemberLanguage):
            returnValue = MemberLanguage.Parse(value);
            break;
            case nameof(MyEnum):
            returnValue = MyEnum.Parse(value);
            break;
            //case nameof(SomeOtherEnum):
            // returnValue = SomeOtherEnum.Parse(value);
            // break;
        }
        return returnValue;
    }
}

Now we create our custom TypeSafeEnumJsonConverter class that implements the JsonConverter object, and will call our conversion helper object. Again, this allows us to keep this converter object isolated, and prevents the need for changes to it should we add additional type safe enum objects

public class TypeSafeEnumJsonConverter : JsonConverter
{        
    public override bool CanConvert(Type objectType)
    {
        var types = new[] { typeof(TypeSafeEnumBase) };
        return types.Any(t => t == objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        string name = objectType.Name;
        string value = serializer.Deserialize(reader).ToString();
        // call our custom conversion object
        return TypeSafeEnumConversion.ConvertToTypeSafeEnum(name, value);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null
            && serializer.NullValueHandling == NullValueHandling.Ignore)
        {
            return;
        }
        writer.WriteValue(value.ToString()); 
    }
}

How it's used...

Create an object that uses our TypeSafeEnums.

public class SomeThing
{
    public string Name { get; set; }

    [JsonProperty("my_enum")]
    [JsonConverter(typeof(TypeSafeEnumJsonConverter))]
    public MyEnum MyEnum { get; set; }

    [JsonProperty("language_preference")]
    [JsonConverter(typeof(TypeSafeEnumJsonConverter))]
    public MemberLanguage LanguagePreference { get; set; }  

    public override string ToString()
    {
        return $"{nameof(SomeThing)} : {MyEnum} : {LanguagePreference}";
    }    
}

Usage...

internal class Program
{
    static void Main(string[] args)
    {
        IList<string> jsonDatas = new List<string>();
        // Serialize
        foreach (MyEnum e in MyEnum.MyEnums())
        {
            var thing = new SomeThing();
            thing.Name = e.ToString().ToUpper();
            thing.LanguagePreference = MemberLanguage.English;
            thing.MyEnum = e;
            string o = JsonConvert.SerializeObject(thing);
            jsonDatas.Add(o);
            Console.WriteLine(o);
        }
        // Deserialize
        foreach (string json in jsonDatas)
        {
            var thing = JsonConvert.DeserializeObject<SomeThing>(json);
            Console.WriteLine(thing.ToString());
        }
    }
}
// Serialized OUTPUT:
// {"Name":"A","my_enum":"a","language_preference":"English"}
// {"Name":"B","my_enum":"b","language_preference":"English"}
// {"Name":"C","my_enum":"c","language_preference":"English"}

// Deserialize OUTPUT:
// SomeThing : a : English
// SomeThing : b : English
// SomeThing : c : English



来源:https://stackoverflow.com/questions/63640608/newtonsoft-json-ignores-typeconverters-and-or-jsonconverters-regardless-of-how

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!