mgholam / fastJSON

Smallest, fastest polymorphic JSON serializer
https://www.codeproject.com/Articles/159450/fastJSON-Smallest-Fastest-Polymorphic-JSON-Seriali
MIT License
479 stars 147 forks source link

Serialise / Deserialise Issue #106

Closed MTantos1 closed 5 years ago

MTantos1 commented 5 years ago

I have a class I'm trying to serialise and then deserialise using your library and it throws an InvalidCastException when trying to Deserialise the following json

[
  {
    "$types": {
      "Common.Lib.LibAddress, LibCommon, Version=1.0.7.0, Culture=neutral, PublicKeyToken=null": "1",
      "Common.Lib.LibSuburb, LibCommon, Version=1.0.7.0, Culture=neutral, PublicKeyToken=null": "2"
    },
    "$type": "1",
    "Street": "PO Box 2034",
    "TheSuburb": {
      "$type": "2",
      "Name": "",
      "Postcode": "",
      "State": "",
      "City": "",
      "Source": "",
      "IsDeleted": false,
      "TimeStamp": "2014-01-31T00:15:31Z",
      "Id": "201"
    },
    "IsDeleted": false,
    "SuburbId": "201",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2014-01-31T00:15:31Z",
    "Id": "540"
  },
  {
    "$type": "1",
    "Street": "PO BOX 260",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "309",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2014-01-31T00:15:32Z",
    "Id": "1393"
  },
  {
    "$type": "1",
    "Street": "43 Lerra Road,",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "436",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2014-01-31T00:15:33Z",
    "Id": "363"
  },
  {
    "$type": "1",
    "Street": "Brixentaler Strasse 24 Hopfgarten Im Brixental\r\nAUSTRIA 6361",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "835",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-08-02T21:06:32Z",
    "Id": "1980"
  },
  {
    "$type": "1",
    "Street": "9 Quokka Court",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "835",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2016-11-16T05:22:12Z",
    "Id": "3194"
  },
  {
    "$type": "1",
    "Street": "PO Box 4884",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "900",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2015-05-27T23:09:09Z",
    "Id": "2198"
  },
  {
    "$type": "1",
    "Street": "Suite 11, Level 1, 1410 Logan Road",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1027",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2016-04-05T05:29:22Z",
    "Id": "2613"
  },
  {
    "$type": "1",
    "Street": "1 Hill Street",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1037",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2016-04-27T00:16:19Z",
    "Id": "2648"
  },
  {
    "$type": "1",
    "Street": "Unit 116, Grand Mariner, 12 Commodore Drive",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1038",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2016-04-28T23:11:40Z",
    "Id": "2650"
  },
  {
    "$type": "1",
    "Street": "\"Grand Mariner\"\r\nUnit 116\r\n12 Commodore Drive",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1038",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2016-05-23T01:05:04Z",
    "Id": "2712"
  },
  {
    "$type": "1",
    "Street": "\"Grand Mariner\"\r\nUnit 116,  12 Commodore Drive",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1038",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2016-05-24T02:40:34Z",
    "Id": "2715"
  },
  {
    "$type": "1",
    "Street": "42 Beacon Hill Road",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1044",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2016-05-02T06:35:25Z",
    "Id": "2664"
  },
  {
    "$type": "1",
    "Street": "8503 Forest Lane",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1214",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-02-28T21:47:09Z",
    "Id": "3398"
  },
  {
    "$type": "1",
    "Street": "Suite 2.2, 6 Reliance Drive",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1233",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-03-27T00:32:35Z",
    "Id": "3501"
  },
  {
    "$type": "1",
    "Street": "Suite 2.2, 6 Reliance Drive",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1233",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-03-27T00:32:42Z",
    "Id": "3502"
  },
  {
    "$type": "1",
    "Street": "42 Macquarie Street",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1251",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-05-02T05:06:11Z",
    "Id": "3582"
  },
  {
    "$type": "1",
    "Street": "131 McIlwraith Street",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1267",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-05-22T06:41:35Z",
    "Id": "3636"
  },
  {
    "$type": "1",
    "Street": "131 McIlwraith Street",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1267",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-05-22T06:41:35Z",
    "Id": "3637"
  },
  {
    "$type": "1",
    "Street": "130 Gould Rd",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1280",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-05-30T23:02:20Z",
    "Id": "3683"
  },
  {
    "$type": "1",
    "Street": "130 Gould Rd",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1280",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-05-30T23:02:20Z",
    "Id": "3684"
  },
  {
    "$type": "1",
    "Street": "54 Waverley Rd",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1288",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-06-08T06:28:18Z",
    "Id": "3710"
  },
  {
    "$type": "1",
    "Street": "10A Holroyd Avenue",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1286",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-06-07T23:19:55Z",
    "Id": "3702"
  },
  {
    "$type": "1",
    "Street": "10A Holroyd Avenue",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1286",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-06-07T23:19:55Z",
    "Id": "3703"
  },
  {
    "$type": "1",
    "Street": "Unit 6, 288 Swann Rd",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1319",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-07-11T02:44:40Z",
    "Id": "3825"
  },
  {
    "$type": "1",
    "Street": "Unit 6, 288 Swann Rd",
    "TheSuburb": {
      "$i": 2
    },
    "IsDeleted": false,
    "SuburbId": "1319",
    "Suburb": "",
    "Postcode": "",
    "State": "",
    "City": "",
    "TimeStamp": "2017-07-11T02:44:40Z",
    "Id": "3826"
  },
  {
    "$type": "1",
    "Street": "__No Address__",
    "TheSuburb": {
      "$type": "2",
      "Name": " ",
      "Postcode": "    ",
      "State": "   ",
      "City": "_Over Seas_",
      "Source": "",
      "IsDeleted": false,
      "TimeStamp": "2014-03-10T02:53:24Z",
      "Id": "457"
    },
    "IsDeleted": false,
    "SuburbId": "457",
    "Suburb": " ",
    "Postcode": "    ",
    "State": "   ",
    "City": "_Over Seas_",
    "TimeStamp": "2014-03-10T02:53:24Z",
    "Id": "1619"
  },
  {
    "$type": "1",
    "Street": "1205 Falls Bridge Dr\r\nRALEIGH NC 27614 USA",
    "TheSuburb": {
      "$i": 28
    },
    "IsDeleted": false,
    "SuburbId": "457",
    "Suburb": " ",
    "Postcode": "    ",
    "State": "   ",
    "City": "_Over Seas_",
    "TimeStamp": "2014-05-05T04:11:19Z",
    "Id": "1694"
  }
]

From what I can see the library appears to be reducing output by using an internal dictionary to serialise "Equal" objects as "$i": n. Based on this assumption the final Address in the json array has a Suburb (replaced by "$i": 28) which throws an InvalidCastException Unable to cast object of type 'Common.Lib.Address' to type 'Common.Lib.Suburb'.

The interface for Address and Suburb are as follows:

public class Address {
    public string Street { get; set; }
    public Suburb TheSuburb { get; set; }
    public bool IsDeleted { get; set; }
    public string SuburbId { get; set; }
    public string Suburb { get; set; }
    public string Postcode { get; set; }
    public string State { get; set; }
    public string City { get; set; }
    public DateTime? TimeStamp { get; set; }
    public string Id { get; set; }
}

public class Suburb {
    public string Name { get; set; }
    public string Postcode { get; set; }
    public string State { get; set; }
    public string City { get; set; }
    public string Source { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? TimeStamp { get; set; }
    public string Id { get; set; }
}
mgholam commented 5 years ago

Is this the exact output from the serializer?

mgholam commented 5 years ago

Can you supply test code?

MTantos1 commented 5 years ago

Is this the exact output from the serializer?

Yes.

Can you supply test code?

Not at the moment, will try put together a small project when I am at a dev machine.

MTantos1 commented 5 years ago

Sorry it has taken me so long to reply. But I found the source of the problem and can replicate with a simple example below. The error arrises when an object "Equal" to that returned by the default constructor exists in the object graph being serialised / deserialised. The serialiser correctly identifies the objects as NonEqual on serialisation (as they have all their properties set at this point), however the Deserialiser, when populating _circobj incorrectly identifies new objects as "Equal" to one already present in the dictionary, as it does this comparison before setting the properties on the object.

The IEqualityComparer used for these dictionaries should be a ReferenceEqual equivalent. See example of ReferenceEqualComparer at StackOverflow.

public class Circle
{
    public Point Center { get; set; }
    public int Radius { get; set; }
}

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point() { X = Y = 0; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override bool Equals(object obj)
    {
        if (obj is Point p) return p.X == X && p.Y == Y;
        return false;
    }

    public override int GetHashCode() => X + Y;
}

public class Tests
{
    [Test]
    public void Test1()
    {
        var circles = new Circle[]
        {
            new Circle() { Center = new Point(0, 0), Radius = 1 },
            new Circle() { Center = new Point(0, 1), Radius = 2 },
            new Circle() { Center = new Point(0, 1), Radius = 3 }
        };
        var json = JSON.ToJSON(cirles);

        circles = JSON.ToObject<Circle[]>(json);
    }
}
mgholam commented 5 years ago

I've added the ReferenceEqualComparer to the object checking and it works however the above code does not output $i the following code does :

var p = new Point(0, 1);
var circles = new Circle[]
{
      new Circle() { Center = new Point(0, 0), Radius = 1 },
      new Circle() { Center = p, Radius = 2 },
      new Circle() { Center = p, Radius = 3 }
};

which is probably what you would expect.

mgholam commented 5 years ago

Weirdly if you remove the Point.Equals() code everything works without the ReferenceEqualComparer.

mgholam commented 5 years ago

Sorry if you remove the Point.GetHashCode() code.

Probably not a good idea to mess with GetHashCode() since from your code it will result in hash conflicts.

MTantos1 commented 5 years ago

Ah, but you must.

It is a requirement for Dictionary to work as intended. Equal objects must have the same hash code. i.e. If a.Equals(b) then a.GetHashCode() == b.GetHashCode() can be inferred with certainty. If I didn't override GetHashCode() then the following would hold:

Point p1 = new Point(0,0);
Point p2 = new Point(0,0);
Console.WriteLine(p1.Equals(p2)); // True
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // False

Which would mean the Dictionary would not behave correctly (when using Points as keys).

I am by no means saying X + Y is a good hash function, but it meets the criteria, and this was just a simple example to show the problem.

Edit: See this post on why GetHashCode() needs to be overridden

mgholam commented 5 years ago

fixed in v2.2.6 behind JSONParameters.OverrideObjectHashCodeChecking = true