Reputation: 9871
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 AllowNullLiteral
attribute 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
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
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
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
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
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