0

I want to serialize and deserialize a custom struct. The serializing seems to work, but deserializing it back doesn't work

This is the struct :

[JsonConverter(typeof(PriceConverter))]
public struct Price
{
    private readonly decimal _value;

    public Price(Price value)
    {
        _value = value._value;
    }

    public Price(decimal value)
    {
        _value = value;
    }

    public static implicit operator decimal(Price p)
    {
        return p._value;
    }

    public static implicit operator Price(decimal d)
    {
        return new Price(d);
    }
}

This is the JsonConverter :

class PriceConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(decimal?);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return serializer.Deserialize(reader) as Price?;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, (decimal)((Price)value));
    }
}

This is the class:

public class Product
{
    public string Name { get; set; }
    public Price Price { get; set; }
}

This is some test code:

Product product1 = new Product();
product1.Name = "Hans";
product1.Price = (decimal)2.3;

string priceSer = JsonConvert.SerializeObject(product1);

Product product2 = JsonConvert.DeserializeObject<Product>(priceSer);

product1 is correct.

?product1.Price

{MvcClient.Features.WFPBranch.Employee.Price}
    _value: 2.3

string priceSer contains : "{"Name":"Hans","Price":2.3}"

product2 however isn't correct. It should contain 2.3 but it contains 0. What is wrong ? ... How can I make it contain 2.3?

?product2.Price

{MvcClient.Features.WFPBranch.Employee.Price}
    _value: 0
dbc
  • 104,963
  • 20
  • 228
  • 340
kahoona
  • 175
  • 2
  • 14

1 Answers1

2

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.

dbc
  • 104,963
  • 20
  • 228
  • 340