【问题标题】:Newtonsoft.Json - DeserializeObject throws when deserializing custom type: Error converting value "somestring" to type CustomTypeNewtonsoft.Json - 反序列化自定义类型时抛出 DeserializeObject:将值“somestring”转换为自定义类型时出错
【发布时间】:2021-03-06 10:05:47
【问题描述】:

我有一个自定义类型:

[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);
    }
}

这里是 Json 序列化/反序列化 (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);
    }

抛出异常:

Newtonsoft.Json.JsonSerializationException:将值“some-friendly-url-1”转换为类型“BlahBlah.Entities.FriendlyUrl”时出错。路径'',第 1 行,位置 21。 ---- System.InvalidCastException : 从“System.String”到“BlahBlah.Entities.FriendlyUrl”的无效转换。

堆栈跟踪:

堆栈跟踪: JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfoculture, 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(字符串值,类型类型,JsonSerializerSettings 设置) JsonConvert.DeserializeObject[T](字符串值,JsonSerializerSettings 设置) JsonConvert.DeserializeObject[T](字符串值)

现在,如果我删除 IConvertible 实现并只留下它确实工作:

  • 显式和隐式 'FriendlyUrl 字符串' 转换运算符。
  • TypeConverter 实现 - 见第一行。

但是当类实现IConvertible 时,我得到了那个错误。我该如何解决这个问题?

(我真的需要IConvertible 来实现。我也尝试使用JsonObject,但没有帮助)。

小提琴

这是重现 - https://dotnetfiddle.net/YPfr60

类型转换器

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);
    }
}

【问题讨论】:

    标签: c# json json.net


    【解决方案1】:

    因为你的类实现了IConvertibleJsonSerializerInternalReader 显然是在尝试调用Convert.ChangeType,而不是使用你提供的TypeConverter。有评论at line 984 of the source code指出Convert.ChangeType不适用于自定义IConvertible,所以作者表面上意识到了这个问题:

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

    您可以通过为您的 FriendlyUrl 类实现自定义 JsonConverter 来解决此问题:

    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());
        }
    }
    

    要使用JsonConverter,只需将[JsonConverter] 属性添加到FriendlyUrl 类,方法与为[TypeConverter] 所做的相同。然后,您可以删除 [TypeConverter] 属性,除非您出于其他目的需要它。 (Json.Net 的DefaultContractResolver 在解析类型时会首先查找JsonConverter,因此它会优先于TypeConverter。)

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

    小提琴:https://dotnetfiddle.net/HyaQWb

    【讨论】:

    • 谢谢!现在我站在绿色的一边:)
    【解决方案2】:

    问题是 Json.NET 似乎处理具有 TypeConverterIConvertible 实现的类型不一致。在某些情况下,类型转换器取代了IConvertible 实现,但在其他情况下,逻辑相反。

    例如:

    • DefaultContractResolver.CreateContract()中,类型转换器优先,因为

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

      之前调用过

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

      (注意注释指出IConvertible 是最后测试的!)

    • 但在JsonSerializerInternalReader.EnsureType() 中,contract.IsConvertable 被提前检查并导致代码沿着不使用类型转换器的路径前进。

    • ConvertUtils.TryConvertInternal() 中,IConvertible 再次被首先检查:

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

      然后很久以后:

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

    解决方案是引入一个custom JsonConverter,覆盖Json.NET的默认行为,如下:

    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;
        }
    }
    

    您现在可以使用以下设置序列化和反序列化 FriendlyUrl

    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);
    

    注意事项:

    • 转换器以通用方式实现,因此所有具有适用TypeConverter和实现IConvertible的类型都使用转换器序列化。

      但是,可能有一些晦涩的系统类型具有 TypeConverter 并实现 IConvertible 并要求在反序列化期间忽略类型转换器。您将需要进行一些生产测试,看看这是否在实践中发生。您可能希望限制转换器仅修复您定义的那些类型。

    • 这些 Newtonsoft 的不一致似乎是错误。您可能想open an issue with Newtonsoft 了解它。

    演示小提琴here.

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-05-07
      • 2016-11-16
      相关资源
      最近更新 更多