The reason your DecimalWithoutTrailingZerosConverter is not used for dictionary keys is that Json.NET does not serialize the keys - it merely converts them to strings. From the docs:
When serializing a dictionary, the keys of the dictionary are converted to strings and used as the JSON object property names. The string written for a key can be customized by either overriding ToString() for the key type or by implementing a TypeConverter. A TypeConverter will also support converting a custom string back again when deserializing a dictionary.
Thus, to get the output you need, you could override the system TypeConverter for decimal as explained here -- but I wouldn't really recommend it since this will change the converter used for decimal everywhere in your application, with various unforseeable consequences.
The alternative would be to write a custom JsonConverter for all dictionaries implementing IDictionary<decimal, TValue> for some 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]);
}
}
}
}
Then use it like:
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);
Note the use of a single converter for all types of decimal dictionary. Note also that the converter uses the existingValue if one is present. Thus if the constructor in your RateCountEvent allocates a sorted dictionary rather than a dictionary, the sorted dictionary will be populated.
Sample fiddle.
Incidentally, you might want to extend your DecimalWithoutTrailingZerosConverter to handle decimal? as well as decimal.
ctorfor your classCountEventis incorrect.decimal, a dictionary instead.CanConvertmethod. shouldn'tDecimalbe too?Int32isn't related to theDictionary<decimal, int>. All the property getters are getting called for their types, but not for that lastInt32consideration.