0

I have encountered a case where Newtonsoft is taking perfectly valid JSON text, but deserializing it incorrectly. I have an object that contains an embedded class that consists of members Year, Month, Week, and DayOfWk. The JSON looks like this:

 "openDate": {
  "Year": 1997,
  "Month": 12,
  "Week": 5,
  "DayOfWk": 5
 },

But the data that comes back after deserialization is Year = 1, Month = 1, Week = 1, and DayOfWk = 1, regardless of the input JSON.

Here is the code (it's in F#, but should be easily readable):

  let jsonText = File.ReadAllText( @"..\..\..\..\Dependencies\ADBE.dat")
  let dailyData = JsonConvert.DeserializeObject<DailyAnalysis[]>(jsonText)

DailyAnalysis is defined as:

type DailyAnalysis = {
openDate: TradeDate
openPrice: Decimal
closeDate: TradeDate
closePrice: Decimal
gainPercentage: Decimal
}

TradeDate is the class in question - it is an F# class that exposes properties Year, Month, Week, and DayOfWk. Year, Month, and Week are int's; DayOfWeek is a DayOfWeek enum. All the other fields in the DailyAnalysis objects come back with the correct values.

How can this problem be resolved?

Note that if I don't include the type in the DeserializeObject call, it does get the correct data, but simply returns it as an object, and converting to the correct type is very difficult (i.e., I don't know how to do it in F#).

Can anybody point out something obvious (or even obscure) I'm overlooking, or point me to other resources?

Update: note that the constructor for TradeDate takes a single DateTime argument.

dbc
  • 104,963
  • 20
  • 228
  • 340
Dave Hanna
  • 2,491
  • 3
  • 32
  • 52
  • Can you share the `TradeDate` type -- i.e. a [mcve]? – dbc Jun 15 '19 at 19:08
  • Can't reproduce with a simple example, see https://dotnetfiddle.net/dxeHt9 – dbc Jun 15 '19 at 19:28
  • All I can guess is that the argument names to the `TradeDate` constructor don't match the property names. Json.NET matches constructor arguments to properties by using the argument name, so if the names are inconsistent a default value is passed in. See https://dotnetfiddle.net/YSvyVy. But without a [mcve] we can only guess. – dbc Jun 15 '19 at 20:35
  • dbc - I think your last point may be the answer - the constructor takes a single DateTime agrument, and of course, Json is not going to have anyway to know what that is. I will explore other ways to structure that. Thanks. – Dave Hanna Jun 15 '19 at 22:50
  • Don't have time to check right now, but could it be an eager eval issue? Change `letJsonText = ...` to `letJsonText () = ...` – Guran Jun 17 '19 at 08:05

1 Answers1

2

Assuming that your TradeDate is immutable (as typically happens in f#), then Json.NET is able to deserialize such a type by finding a single constructor which is parameterized, then invoking it by matching constructor arguments to JSON properties by name, modulo case. Arguments that do not match are given a default value. If TradeDate actually takes a single DateTime as input, you will get the behavior you are seeing.

For instance, if we take a simplified version like so:

type TradeDate(date : DateTime) = 
    member this.Year = date.Year
    member this.Month = date.Month
    member this.DayOfMonth = date.Day

And then round-trip it using Json.NET as follows:

let t1 = new TradeDate(new DateTime(1997, 12, 25))
let json1 = JsonConvert.SerializeObject(t1)
let t2 = JsonConvert.DeserializeObject<TradeDate>(json1)
let json2 = JsonConvert.SerializeObject(t2)

printfn "\nResult after round-trip:\n%s" json2

The result becomes:

{"Year":1,"Month":1,"DayOfMonth":1}

Which is exactly what you are seeing. Demo fiddle #1 here.

So, what are your options? Firstly, you could modify TradeDate to have the necessary constructor, and mark it with JsonConstructor. It could be private as long as the attribute is applied:

type TradeDate [<JsonConstructor>] private(year : int, month : int, dayOfMonth: int) = 
    member this.Year = year
    member this.Month = month
    member this.DayOfMonth = dayOfMonth

    new(date : DateTime) = new TradeDate(date.Year, date.Month, date.Day)

Demo fiddle #2 here.

Secondly, if you cannot modify TradeDate or add Json.NET attributes to it, you could introduce a custom JsonConverter for it:

[<AllowNullLiteral>] type private TradeDateDTO(year : int, month : int, dayOfMonth : int) =
    member this.Year = year
    member this.Month = month
    member this.DayOfMonth = dayOfMonth

type TradeDateConverter () =
    inherit JsonConverter()

    override this.CanConvert(t) = (t = typedefof<TradeDate>)

    override this.ReadJson(reader, t, existingValue, serializer) = 
        let dto = serializer.Deserialize<TradeDateDTO>(reader)
        match dto with
        | null -> raise (new JsonSerializationException("null TradeDate"))
        | _ -> new TradeDate(new DateTime(dto.Year, dto.Month, dto.DayOfMonth)) :> Object

    override this.CanWrite = false

    override this.WriteJson(writer, value, serializer) = 
        raise (new NotImplementedException());

And deserialize as follows:

let converter = new TradeDateConverter()
let t2 = JsonConvert.DeserializeObject<TradeDate>(json1, converter)

Demo fiddle #3 here.

Notes:

  1. Your question did not include code for TradeDate, in particular the code for converting between a DateTime and the year/month/week of month/day of week representation. This turns out to be slightly nontrivial so I did not include it in the answer; see Calculate week of month in .NET and Calculate date from week number for how this might be done.

  2. For details on how Json.NET chooses which constructor to invoke for a type with multiple constructors, see How does JSON deserialization in C# work.

dbc
  • 104,963
  • 20
  • 228
  • 340