Newtonsoft.Json - DeserializeObject throws when deserializing custom type: Error converting value “somestring” to type CustomType

和自甴很熟 提交于 2021-02-10 05:14:41

问题


I have a custom type:

[TypeConverter(typeof(FriendlyUrlTypeConverter))]
public class FriendlyUrl : IEquatable<FriendlyUrl>, IConvertible
{
    public FriendlyUrl()
    {
        _friendlyUrl = string.Empty;
    }

    public FriendlyUrl(string value)
    {
        value = value.Trim();
        if (!FriednlyUrlValidator.Validate(value))
            throw new FriendlyUrlValidationException("Invalid value for FrienlyUrl");
        _friendlyUrl = value;
    }

    public static implicit operator FriendlyUrl(string friendlyUrlValue)
    {
        if (friendlyUrlValue != "" && !FriednlyUrlValidator.Validate(friendlyUrlValue))
            throw new FriendlyUrlValidationException($"Invalid value for FrienlyUrl: {friendlyUrlValue}");
        return new FriendlyUrl { _friendlyUrl = friendlyUrlValue };
    }

    public static implicit operator string(FriendlyUrl furl)
    {
        return furl._friendlyUrl;
    }

    public override string ToString()
    {
        return _friendlyUrl;
    }

    private string _friendlyUrl;

//[...skip IEquatable implementation...]

    TypeCode IConvertible.GetTypeCode()
    {
        return _friendlyUrl.GetTypeCode();
    }

    bool IConvertible.ToBoolean(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToBoolean(provider);
    }

    byte IConvertible.ToByte(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToByte(provider);
    }

    char IConvertible.ToChar(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToChar(provider);
    }

    DateTime IConvertible.ToDateTime(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToDateTime(provider);
    }

    decimal IConvertible.ToDecimal(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToDecimal(provider);
    }

    double IConvertible.ToDouble(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToDouble(provider);
    }

    short IConvertible.ToInt16(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToInt16(provider);
    }

    int IConvertible.ToInt32(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToInt32(provider);
    }

    long IConvertible.ToInt64(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToInt64(provider);
    }

    sbyte IConvertible.ToSByte(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToSByte(provider);
    }

    float IConvertible.ToSingle(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToSingle(provider);
    }

    string IConvertible.ToString(IFormatProvider provider)
    {
        return _friendlyUrl.ToString(provider);
    }

    object IConvertible.ToType(Type conversionType, IFormatProvider provider)
    {
        if (conversionType == typeof(FriendlyUrl))
            return this;
        return ((IConvertible) _friendlyUrl).ToType(conversionType, provider);
    }

    ushort IConvertible.ToUInt16(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToUInt16(provider);
    }

    uint IConvertible.ToUInt32(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToUInt32(provider);
    }

    ulong IConvertible.ToUInt64(IFormatProvider provider)
    {
        return ((IConvertible) _friendlyUrl).ToUInt64(provider);
    }
}

and here's the test for Json serialization/deserialization (xUnit):

    [Fact]
    public void ConvertToJsonAndBack()
    {
        FriendlyUrl friendlyUrl = "some-friendly-url-1";
        string friendlyUrlJson = JsonConvert.SerializeObject(friendlyUrl);
        Assert.Equal($"\"{friendlyUrl}\"", friendlyUrlJson);

        // ******** Throws the next line: ********
        FriendlyUrl deserialized = JsonConvert.DeserializeObject<FriendlyUrl>(friendlyUrlJson);
        Assert.Equal(friendlyUrl, deserialized);
    }

It throws with the exception:

Newtonsoft.Json.JsonSerializationException : Error converting value "some-friendly-url-1" to type 'BlahBlah.Entities.FriendlyUrl'. Path '', line 1, position 21. ---- System.InvalidCastException : Invalid cast from 'System.String' to 'BlahBlah.Entities.FriendlyUrl'.

Stack Trace:

Stack Trace: JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType) JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent) JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType) JsonSerializer.Deserialize(JsonReader reader, Type objectType) JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings) JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings) JsonConvert.DeserializeObject[T](String value)

Now, it does work if I remove IConvertible implementation and leave only:

  • explicit and implicit 'FriendlyUrl <=> string' conversion operators.
  • TypeConverter implementation - see the very first line.

But when the class implements IConvertible I get that error. How can I fix this?

(I really need IConvertible to be implemented. I also tried to use JsonObject and it didn't help).

Fiddle

Here's the repro - https://dotnetfiddle.net/YPfr60.

TypeConverter

public class FriendlyUrlTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        return value is string sValue ? new FriendlyUrl(sValue) : base.ConvertFrom(context, culture, value);
    }

    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        return destinationType == typeof(string) ? value.ToString() : base.ConvertTo(context, culture, value, destinationType);
    }
}

回答1:


Because your class implements IConvertible, the JsonSerializerInternalReader is apparently attempting to call Convert.ChangeType instead of using the TypeConverter you supplied. There is a comment at line 984 of the source code stating that Convert.ChangeType does not work for a custom IConvertible, so the author is ostensibly aware of the issue:

if (contract.IsConvertable)
{
    JsonPrimitiveContract primitiveContract = (JsonPrimitiveContract)contract;

    ...

    // this won't work when converting to a custom IConvertible
    return Convert.ChangeType(value, contract.NonNullableUnderlyingType, culture);
}

You can work around the problem by implementing a custom JsonConverter for your FriendlyUrl class:

public class FriendlyUrlJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(FriendlyUrl);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return new FriendlyUrl((string)reader.Value);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(((FriendlyUrl)value).ToString());
    }
}

To use the JsonConverter, simply add a [JsonConverter] attribute to your FriendlyUrl class in the same way that you did for [TypeConverter]. You can then remove the [TypeConverter] attribute, unless you need it for some other purpose. (Json.Net's DefaultContractResolver looks for a JsonConverter first when resolving types, so it will take precedence over TypeConverter.)

[JsonConverter(typeof(FriendlyUrlJsonConverter))]
public class FriendlyUrl : IEquatable<FriendlyUrl>, IConvertible
{
    ...
}

Fiddle: https://dotnetfiddle.net/HyaQWb




回答2:


The problem is that Json.NET seems to handle types with both a TypeConverter and an IConvertible implementation inconsistently. In some cases, the type converter supersedes the IConvertible implementation, but in others the logic is reversed.

For instance:

  • in DefaultContractResolver.CreateContract(), the type converter takes precedence because

         if (CanConvertToString(t))
         {
             return CreateStringContract(objectType);
         }
    

    is called before

         // tested last because it is not possible to automatically deserialize custom IConvertible types
         if (IsIConvertible(t))
         {
             return CreatePrimitiveContract(t);
         }
    

    (Notice the comment that indicates that IConvertible is tested last!)

  • But in JsonSerializerInternalReader.EnsureType(), contract.IsConvertable is checked early and causes the code to go down a path in which the type converter is not used.

  • And in ConvertUtils.TryConvertInternal(), IConvertible is again checked first:

         // use Convert.ChangeType if both types are IConvertible
         if (IsConvertible(initialValue.GetType()) && IsConvertible(targetType))
         {
             // Calls System.Convert.ChangeType(initialValue, targetType, culture);
    

    Then much later:

         // see if source or target types have a TypeConverter that converts between the two
         TypeConverter fromConverter = TypeDescriptor.GetConverter(targetType);
    

The solution is to introduce a custom JsonConverter that overrides Json.NET's default behavior, such as the following:

public class FixIConvertibleConverter : JsonConverter
{
    readonly IContractResolver resolver;

    public FixIConvertibleConverter() : this(JsonSerializer.CreateDefault().ContractResolver) { }

    public FixIConvertibleConverter(IContractResolver resolver)
    {
        if (resolver == null)
            throw new ArgumentNullException();
        this.resolver = resolver;
    }

    public override bool CanConvert(Type objectType)
    {
        var type = Nullable.GetUnderlyingType(objectType) ?? objectType;

        if (!typeof(IConvertible).IsAssignableFrom(type))
            return false;
        // Only the Type type and types with type converters get assigned JsonStringContract
        var contract = resolver.ResolveContract(type) as JsonStringContract;  
        if (contract == null)
            return false;
        if (contract.Converter != null)
            return false;
        var converter = TypeDescriptor.GetConverter(type);
        //converter.ConvertFromInvariantString()
        if (!converter.CanConvertTo(typeof(string)))
            return false;
        //converter.ConvertToInvariantString();
        if (!converter.CanConvertFrom(typeof(string)))
            return false;
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;
        var type = Nullable.GetUnderlyingType(objectType) ?? objectType;
        if (reader.TokenType != JsonToken.String)
            throw new JsonSerializationException();
        var s = (string)reader.Value;
        return TypeDescriptor.GetConverter(type).ConvertFromInvariantString(s);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(TypeDescriptor.GetConverter(value.GetType()).ConvertToInvariantString(value));
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

You can now serialize and deserialize FriendlyUrl with the following settings:

var settings = new JsonSerializerSettings
{
    Converters = { new FixIConvertibleConverter() },
};

FriendlyUrl friendlyUrl = "some-friendly-url-1";
string friendlyUrlJson = JsonConvert.SerializeObject(friendlyUrl, settings);
FriendlyUrl deserialized = JsonConvert.DeserializeObject<FriendlyUrl>(friendlyUrlJson, settings);

Notes:

  • The converter is implemented in a general way so that all types that have an applicable TypeConverter and implement IConvertible are serialized using the converter.

    However, there might be some obscure system type(s) that have a TypeConverter and implement IConvertible and require the type converter to be ignored during deserialization. You will need to do some production testing to see whether this occurs in practice. You might want to restrict the converter to fix only those types you define.

  • These Newtonsoft inconsistencies seem like bugs. You might want to open an issue with Newtonsoft about it.

Demo fiddle here.



来源:https://stackoverflow.com/questions/64972694/newtonsoft-json-deserializeobject-throws-when-deserializing-custom-type-error

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