问题
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 implementIConvertible
are serialized using the converter.However, there might be some obscure system type(s) that have a
TypeConverter
and implementIConvertible
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