我有一个包含IDictionary<decimal, int>
属性的简单类型:
public class CountEvent
{
[JsonProperty(PropertyName = "_ts")]
public DateTime Timestamp { get; set; }
public String Pair { get; set; }
[JsonProperty(PropertyName = "dir")]
public String Direction { get; set; }
public IDictionary<Decimal, Int32> Data { get; set; }
public RateCountEvent()
{
Data = new Dictionary<Decimal, Int32>();
}
}
我故意使用IDictionary,因为我在运行时提供了Dictionary或SortedDictionary实例。将属性类型更改为Dictionary类不会影响行为。
我想为Decimal提供自定义序列化逻辑(即删除尾随零),并为此编写了一个类:
public class DecimalWithoutTrailingZerosConverter: JsonConverter
{
private readonly IFormatProvider formatProvider;
public DecimalWithoutTrailingZerosConverter(IFormatProvider formatProvider)
{
this.formatProvider = formatProvider;
}
public override Boolean CanConvert(Type objectType)
{
return objectType == typeof(Decimal);
}
public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.String)
throw new Exception("Wrong Token Type");
return Convert.ToDecimal(reader.Value, formatProvider);
}
public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
{
String val;
if (value is decimal)
{
val = ((Decimal) value).ToString("G29", formatProvider);
}
else
{
throw new Exception("Expected date object value.");
}
writer.WriteValue(val);
}
}
是的,我知道JSON序列化通常是文化不变的。这不是我的观点。
问题是,当我将转换器添加到序列化设置时,它被认为是(CanConvert
方法被调试器命中),但是十进制类型从未在那里显示为{{1参数,而所有其他类型都是(我的类型CountEvent,DateTime,String,Dictionary(我认为它考虑运行时类型)和Int32)。
我查看了库代码,似乎objectType
类中有一个自定义逻辑序列化字典(我假设)。
问题是 - 我看到的行为是正确的吗?可以覆盖吗?
附: 我不是说图书馆是错的(毕竟,成千上万的人每天都在使用它),我只是想找到一种方法让它适用于这种情况。
答案 0 :(得分:1)
您的DecimalWithoutTrailingZerosConverter
未用于字典键的原因是Json.NET没有序列化键 - 它只是将它们转换为字符串。来自docs:
序列化字典时,字典的键将转换为字符串并用作JSON对象属性名称。为密钥编写的字符串可以通过覆盖密钥类型的ToString()或通过实现TypeConverter来自定义。在反序列化字典时,TypeConverter还将支持再次转换自定义字符串。
因此,为了获得您需要的输出,您可以按照here覆盖TypeConverter
的系统decimal
- 但我不会真的推荐它,因为这会改变转换器用于应用程序中的decimal
,带来各种不可预见的后果。
替代方法是为所有IDictionary<decimal, TValue>
TValue
实施public class DecimalDictionaryWithoutTrailingZerosConverter : DecimalWithoutTrailingZerosConverterBase
{
public DecimalDictionaryWithoutTrailingZerosConverter(IFormatProvider formatProvider) : base(formatProvider) { }
public override Boolean CanConvert(Type objectType)
{
var types = objectType.GetDictionaryKeyValueTypes().ToList();
return types.Count == 1 && types[0].Key == typeof(Decimal);
}
object ReadJsonGeneric<TValue>(JsonReader reader, Type objectType, IDictionary<decimal, TValue> existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartObject)
throw new JsonSerializationException("Invalid object type " + reader.TokenType);
if (existingValue == null)
{
var contract = serializer.ContractResolver.ResolveContract(objectType);
existingValue = (IDictionary<decimal, TValue>)contract.DefaultCreator();
}
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
case JsonToken.PropertyName:
{
var name = reader.Value.ToString();
var key = TokenToDecimal(JsonToken.String, name);
if (!reader.Read())
throw new JsonSerializationException(string.Format("Missing value at path: {0}", reader.Path));
var value = serializer.Deserialize<TValue>(reader);
existingValue.Add(key, value);
}
break;
case JsonToken.EndObject:
return existingValue;
default:
throw new JsonSerializationException(string.Format("Unknown token {0} at path: {1} ", reader.TokenType, reader.Path));
}
}
throw new JsonSerializationException(string.Format("Unclosed object at path: {0}", reader.Path));
}
public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
try
{
var keyValueTypes = objectType.GetDictionaryKeyValueTypes().Single(); // Throws an exception if not exactly one.
var method = GetType().GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
var genericMethod = method.MakeGenericMethod(new[] { keyValueTypes.Value });
return genericMethod.Invoke(this, new object[] { reader, objectType, existingValue, serializer });
}
catch (Exception ex)
{
if (ex is JsonException)
throw;
// Wrap the TypeInvocationException in a JsonSerializerException
throw new JsonSerializationException("Failed to deserialize " + objectType, ex);
}
}
void WriteJsonGeneric<TValue>(JsonWriter writer, IDictionary<decimal, TValue> value, JsonSerializer serializer)
{
writer.WriteStartObject();
foreach (var pair in value)
{
writer.WritePropertyName(DecimalToToken(pair.Key));
serializer.Serialize(writer, pair.Value);
}
writer.WriteEndObject();
}
public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
{
try
{
var keyValueTypes = value.GetType().GetDictionaryKeyValueTypes().Single(); // Throws an exception if not exactly one.
var method = GetType().GetMethod("WriteJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
var genericMethod = method.MakeGenericMethod(new[] { keyValueTypes.Value });
genericMethod.Invoke(this, new object[] { writer, value, serializer });
}
catch (Exception ex)
{
if (ex is JsonException)
throw;
// Wrap the TypeInvocationException in a JsonSerializerException
throw new JsonSerializationException("Failed to serialize " + value, ex);
}
}
}
public class DecimalWithoutTrailingZerosConverter : DecimalWithoutTrailingZerosConverterBase
{
public DecimalWithoutTrailingZerosConverter(IFormatProvider formatProvider) : base(formatProvider) { }
public override Boolean CanConvert(Type objectType)
{
return objectType == typeof(Decimal);
}
public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
{
return TokenToDecimal(reader.TokenType, reader.Value);
}
public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
{
writer.WriteValue(DecimalToToken(value));
}
}
public abstract class DecimalWithoutTrailingZerosConverterBase : JsonConverter
{
private readonly IFormatProvider formatProvider;
public DecimalWithoutTrailingZerosConverterBase(IFormatProvider formatProvider)
{
this.formatProvider = formatProvider;
}
protected string DecimalToToken(decimal value)
{
return value.ToString("G29", formatProvider);
}
protected string DecimalToToken(object value)
{
if (value is decimal)
{
return DecimalToToken((Decimal)value);
}
else
{
throw new JsonSerializationException("Expected date object value.");
}
}
protected decimal TokenToDecimal(JsonToken tokenType, object value)
{
if (tokenType != JsonToken.String)
throw new JsonSerializationException("Wrong Token Type");
return Convert.ToDecimal(value, formatProvider);
}
}
public static class TypeExtensions
{
/// <summary>
/// Return all interfaces implemented by the incoming type as well as the type itself if it is an interface.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
{
if (type == null)
throw new ArgumentNullException();
if (type.IsInterface)
return new[] { type }.Concat(type.GetInterfaces());
else
return type.GetInterfaces();
}
public static IEnumerable<KeyValuePair<Type, Type>> GetDictionaryKeyValueTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
{
var args = intType.GetGenericArguments();
if (args.Length == 2)
yield return new KeyValuePair<Type, Type>(args[0], args[1]);
}
}
}
}
的所有词典编写custom JsonConverter
:
var culture = new CultureInfo("de-DE");
var settings = new JsonSerializerSettings
{
Converters = new JsonConverter[] { new DecimalWithoutTrailingZerosConverter(culture), new DecimalDictionaryWithoutTrailingZerosConverter(culture) },
Formatting = Formatting.Indented,
};
var json = JsonConvert.SerializeObject(rateCountEvent, settings);
然后使用它:
existingValue
请注意,对所有类型的十进制字典使用单个转换器。另请注意,转换器使用RateCountEvent
(如果存在)。因此,如果DecimalWithoutTrailingZerosConverter
中的构造函数分配排序的字典而不是字典,则将填充已排序的字典。
示例fiddle。
顺便说一句,您可能希望将decimal?
扩展为处理decimal
以及{{1}}。