Your basic problem is that, in ReadJson()
, serializer.Deserialize(reader)
does not return what you think. We can demonstrate that by debugging as follows:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var v = serializer.Deserialize(reader);
Console.WriteLine(v.GetType()); // prints System.Double
return v as Price?;
}
The value of v
here turns out to be double
not decimal
. And since your Price
type does not have an implicit or explicit conversion from double
, v as Price?
returns a null nullable value.
Demo fiddle #1 here.
Now, you might think you could resolve the problem by deserializing explicitly to decimal
as follows:
class PriceConverter : JsonConverter<Price>
{
public override Price ReadJson(JsonReader reader, Type objectType, Price existingValue, bool hasExistingValue, JsonSerializer serializer) =>
serializer.Deserialize<decimal>(reader);
public override void WriteJson(JsonWriter writer, Price value, JsonSerializer serializer) =>
serializer.Serialize(writer, (decimal)value);
}
And sure enough, Price._value
will be deserialized. But in doing so, a subtle bug is introduced, namely that, at the time that ReadJson()
is called, JsonTextReader
will already have recognized the incoming value as a double
and discarded the raw JSON token. Thus the additional precision of decimal
will have been lost.
Demo fiddle #2 here which shows that a price value of 2.30m
is round-tripped as 2.3m
.
So what are your options to avoid this problem?
Firstly, you could serialize and deserialize globally with JsonSerializerSettings.FloatParseHandling = FloatParseHandling.Decimal
, and also modify your converter to throw an exception in the event that the current value was already recognized to be double
:
class PriceConverter : JsonConverter<Price>
{
public override Price ReadJson(JsonReader reader, Type objectType, Price existingValue, bool hasExistingValue, JsonSerializer serializer) =>
reader.ValueType == typeof(double) ? throw new JsonSerializationException("Double value for Price") : serializer.Deserialize<decimal>(reader);
public override void WriteJson(JsonWriter writer, Price value, JsonSerializer serializer) =>
serializer.Serialize(writer, (decimal)value);
}
var settings = new JsonSerializerSettings
{
FloatParseHandling = FloatParseHandling.Decimal,
};
string priceSer = JsonConvert.SerializeObject(product1, settings);
Product product2 = JsonConvert.DeserializeObject<Product>(priceSer, settings);
Of course, this might have unexpected side-effects on how types containing double
values are deserialized.
Demo fiddle #3 here.
Secondly, you could apply FloatParseHandlingConverter
from this answer to Force decimal type in class definition during serialization to every class containing a Price
and modify your converter to throw an exception as in the first option:
[JsonConverter(typeof(FloatParseHandlingConverter), FloatParseHandling.Decimal)]
public class Product
{
public string Name { get; set; }
public Price Price { get; set; }
}
This avoids the need to make global changes to serialization settings but might be somewhat burdensome if your Price
type is used widely.
Demo fiddle #4 here.
Thirdly, if you are flexible about your JSON format, you could serialize Price
as an object containing the price value, rather than a value:
class PriceConverter : JsonConverter<Price>
{
class PriceDTO { public decimal Value { get; set; } }
public override Price ReadJson(JsonReader reader, Type objectType, Price existingValue, bool hasExistingValue, JsonSerializer serializer) =>
serializer.Deserialize<PriceDTO>(reader).Value;
public override void WriteJson(JsonWriter writer, Price value, JsonSerializer serializer) =>
serializer.Serialize(writer, new PriceDTO { Value = value });
}
This avoids the need to make any changes to FloatParseHandling
outside the scope of Price
, but does result in more complex JSON
{"Name":"Hans","Price":{"Value":2.30}}
Instead of
{"Name":"Hans","Price":2.30}
Demo fiddle #5 here.
Notes:
Your PriceConverter.CanConvert(Type objectType)
returns true
in the event that the incoming type is typeof(decimal?)
, but it should instead check for typeof(Price)
.
CanConvert()
is not called when the converter is applied directly via attributes, however, so this mistake is not symptomatic.
Alternatively, if you inherit from JsonConverter<Price>
rather than JsonConverter
, you won't have to override CanConvert()
at all, and your code will be simpler and cleaner.