Wesley Wiser
Wesley Wiser

Reputation: 9871

Record Equals or GetHashCode throws NullReferenceException

I have a number of records that look like this:

[<DataContract>]
type Rec1 = {
    [<DataMember>] mutable field1 : int;
    [<DataMember>] mutable field2 : string;
}

[<DataContact>]
type Rec2 = { 
    [<DataMember>] mutable field3 : Rec1;
    [<DataMember>] mutable field4 : int;
}

I use DataContactJsonSerializer to deserialize JSON into this structure. This is a valid JSON value:

{ "field3": null, "field4": 1 }

Which means at runtime, field3 is null/Unchecked.defaultOf<_>. In Visual Studio 2010, this test works fine:

(deserialize<Rec2> "{ field3: null, field4: 1 }") = { field3 = Unchecked.defaultOf<_>; field4 = 1 } //true

In Visual Studio 2013, the same code throws a NullReferenceException:

at Rec2.Equals(Rec2 obj) 

Peeking at the code in ILSpy, I see this is generated:

if(this != null)
{
    return obj != null && this.field3.Equals(obj.field3) && this.field4.Equals(obj.field4);
}
return obj == null;

So the issue is the compiler assumes that field3 is never null which isn't the case since DataContractJsonSerializer sets the value to null. I've tried applying the AllowNullLiteralattribute to Rec1 but F# records aren't allowed to have that attribute applied to them. How can I either tell the compiler that the fields can be null or re-structure my types to allow this to work?

Upvotes: 6

Views: 438

Answers (3)

Wallace
Wallace

Reputation: 17488

I ran into this problem on a project. In my opinion, it is a bug in the generated code for Equal and GetHashCode.

A workaround for me was to replace the null values immediately after deserialization. See the removeNulls function that I have added to your sample code:

[<DataContract>]
type Rec1 =
    {
        [<DataMember>] mutable field1 : int
        [<DataMember>] mutable field2 : string
    }
    static member empty = { field1 = 0; field2 = String.Empty }

[<DataContract>]
type Rec2 =
    { 
        [<DataMember>] mutable field3 : Rec1;
        [<DataMember>] mutable field4 : int;
    }
    static member removeNulls (rec2 : Rec2) =
        { rec2 with
            field3 = if Object.ReferenceEquals(rec2.field3, null)
                         then Rec1.empty
                         else rec2.field3
        }

      :
      :         

let rec2 = deserialize<Rec2> json
let rec2' = Rec2.removeNulls rec2

This solves the problem throughout your use of the Rec2 instance -- not just when the framework happens to call Equal or GetHashCode. Otherwise, you will have to check that field3 is not null whenever you access it in your code too.

Upvotes: 0

J D
J D

Reputation: 48707

The problem seems to be that you think null is a valid value of the Rec1 type but it isn't. That value is not valid there so you are feeding garbage data into F# code.

You'll get similar behaviour if you feed null into any code where it is not expected. For example:

2::1::Unchecked.defaultof<_>

prints [2] which is just nonsense. Garbage in, garbage out...

Upvotes: -1

JaredPar
JaredPar

Reputation: 755457

F# is generating the types assuming it will only ever be used from F# where null isn't possible. Because [<AllowNullLiteral>] isn't allowed on that element I don't think there is a way to control code gen such that it will account for this possibility. I can think of 2 approaches to fixing this

  1. Implement custom equality on the Rec2 instead of the default structural equality. This means you have to write the equality method yourself which is unfortunate but it should allow you to account for the possibility of null
  2. Change the code to generate struct instances instead of class. This eliminates the possibility of null in any CLR use case. This will require you to move from a record definition to a full type though

Upvotes: 8

Related Questions