jboy12
jboy12

Reputation: 3916

Deserializing a class with a field of type delegate

I have a class with a field that is a delegate that I would like to serialize and deserialize.

It looks like so:

public delegate bool DistanceEqualityStrategy (Distance distance1, Distance distance2);

[JsonObject(MemberSerialization.Fields)]
public partial class Distance
{

    private double _intrinsicValue;

    [JsonIgnore]
    private DistanceEqualityStrategy _strategy;

    public Distance(double passedInput, DistanceEqualityStrategy passedStrategy = null)
    {
        _intrinsicValue = passedInput;
        _equalityStrategy = _chooseDefaultOrPassedStrategy(passedStrategy);
    }

    public Distance(Distance passedDistance)
    {
        _intrinsicValue = passedDistance._intrinsicValue;
        _equalityStrategy = passedDistance._equalityStrategy;
    }

    private static DistanceEqualityStrategy _chooseDefaultOrPassedStrategy(DistanceEqualityStrategy passedStrategy)
    {
        if (passedStrategy == null)
        {
            return EqualityStrategyImplementations.DefaultConstantEquality;
        }
        else
        {
            return passedStrategy;
        }
    }
}

I am having trouble understanding what is really going on when deserializing this object. How does the object get rebuilt? Does it call a specific constructor of the class when it attempts to recreate the object? It serializes just fine, but when the object is rebuilt, it sets the delegate to be null.

This leads me to the conclusion that I do not understand how this deserialization works, as it does not seem to use a constructor.

Can someone explain to me how an object is created on deserialization without using the constructors?

Upvotes: 1

Views: 2431

Answers (2)

dbc
dbc

Reputation: 116991

You asked, Can someone explain to me how an object is created on deserialization without using the constructors?

The short answer is, FormatterServices.GetUninitializedObject() can be used do do this.

The POCO type shown in the question has been marked with [JsonObject(MemberSerialization.Fields)]. Applying MemberSerialization.Fields to a POCO type that maps to a JSON object triggers special a special construction rule. The rules for how Json.NET decides how to construct an instance of such a type are as follows:

  1. If [JsonConstructor] is set on a constructor, use that constructor.

  2. Next, in full trust only, when MemberSerialization.Fields is applied, or [Serializable] is applied and DefaultContractResolver.IgnoreSerializableAttribute == false, the special method FormatterServices.GetUninitializedObject() is used to allocate the object. None of the type's constructors are called.

  3. Next, if there is a public parameterless constructor, use it.

  4. Next, if there is a non-public parameterless constructor and JsonSerializerSettings.ConstructorHandling == ConstructorHandling.AllowNonPublicDefaultConstructor, use it.

  5. Next, if there is a single public parameterized constructor, use that constructor, matching the JSON object properties to constructor parameters by name (modulo case) then deserializing the matched properties to the constructor parameter type. Default values are passed when there is no matching JSON object property.

  6. Failing all of the above, the object cannot be constructed and an exception will get thrown during deserialization.

See the reference source for the logic. Dictionaries, dynamic objects, types that implement ISerializable, collections (types that map to a JSON array) and types that map to JSON primitives may have somewhat different rules.

Thus FormatterServices.GetUninitializedObject() is called instead of the type's constructors, and so its _equalityStrategy is never initialized.

As a workaround, since you are not serializing _equalityStrategy anyway, you can add an OnDeserialized callback that initializes it:

[JsonObject(MemberSerialization.Fields)]
public partial class Distance
{
    [JsonIgnore]
    private DistanceEqualityStrategy _equalityStrategy;

    [OnDeserialized]
    void OnDeserialized(StreamingContext context)
    {
        this._equalityStrategy = _chooseDefaultOrPassedStrategy(this._equalityStrategy);
    }
}

This behavior of Json.NET is not clearly documented. The most relevant paragraphs are from the Serialization Guide:

Finally, types can be serialized using a fields mode. All fields, both public and private, are serialized and properties are ignored. This can be specified by setting MemberSerialization.Fields on a type with the JsonObjectAttribute or by using the .NET SerializableAttribute and setting IgnoreSerializableAttribute on DefaultContractResolver to false.

And from the Json.NET 4.5 Release 8 release notes:

Change - The serializer now creates objects using GetUninitializedObject when deserializing a Serializable type.

In fact, GetUninitializedObject() is used in "fields mode", likely in order to emulate the behavior of DataContractJsonSerializer. You could report a documentation issue if you like.

Upvotes: 1

Ben Voigt
Ben Voigt

Reputation: 283733

I don't think delegates have anything to do with your question, which appears to be:

Can I provide custom logic for setting fields marked with the JsonIgnore attribute? (since they aren't contained in the serialization stream)

Unfortunately the JsonIgnoreAttribute documentation is very scarce and doesn't explain this. Perhaps it is covered elsewhere.

As for your question about what constructor is used, it appears to be controlled by the ConstructorHandling property on the JsonSerializer instance.

The CustomCreationConverter class also sounds like it might be relevant, since it is described as "provides a way to customize how an object is created during JSON deserialization".

Upvotes: 0

Related Questions