CGR
CGR

Reputation: 101

JSON.NET custom constructor

as a partial problem of this question JSON.NET CustomCreationConverter with nested objects I tried to call a custom constructor during deserialization. My simplified class hierarchy is as follows:

public abstract class BusinessObjectBase
{
    internal BusinessObjectBase(SerializationContext context)
        : base(context)
    {
    }
}

public abstract class EditableObjectBase : BusinessObjectBase
{
    protected EditableObjectBase(SerializationContext context)
        : base(context)
    {
    }
}

public class EditableObjectCollection<TObject> : BusinessObjectBase, ICollection<TObject>, IList, INotifyCollectionChanged where TObject : BusinessObjectBase
{
    protected EditableObjectCollection(SerializationContext context)
        : base(context)
    {
    }
}

I know the object hierarchy to a certain level, but users are allowed / forced to derive their own classes. My idea was to write a custom creation converter. The problem I'm fighting with is that a property in a serialized object can be declared as BusinessObjectBase which is abstract, but the real object will be a more derived class and might be a collection or not. A CustomCreationConverter only gets this abstract type passed to the Create method and of course can't create the correct type from this information.

Inspired from this How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects I implemented a converter as follows:

internal class BusinessObjectCreationConverter : JsonConverter
{
    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(BusinessObjectBase).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        object result = null;

        if (reader.TokenType != JsonToken.Null)
        {
            JObject jsonObject = JObject.Load(reader);
            result = this.Create(objectType, jsonObject);
            Verification.Assert<NullReferenceException>(result != null, "No Business Object created.");
            serializer.Populate(jsonObject.CreateReader(), result);
        }

        return result;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
    }

    public BusinessObjectBase Create(Type objectType, JObject jsonObject)
    {
        JToken token       = jsonObject.SelectToken("$type");
        var typeString     = token.Value<string>();
        Type type          = Type.GetType(typeString);
        var businessObject = type.CreateUsingDesrializationConstructor<BusinessObjectBase>();

        businessObject.Initialize(true);
        return businessObject;
    }
}

My class to test serialization looks like this:

public class AnyPocoContainingBusinessObject
{
    public BusinessObjectBase BusinessObject { get; set; }    
}

public class TestEditableObject : EditableObjectBase
{
    internal TestEditableObject(SerializationContext context)
        : base(context)
    {
    }
 }

If I initialize my class my class with an collection

var collection = new EditableObjectCollection<TestEditableObject>(null);
var poco = new AnyPocoContainingBusinessObject { BusinessObject = collection };

and configure the serializer this way:

    public NewtonsoftJsonSerializer()
        : this(new JsonSerializer
        {
            TypeNameHandling = TypeNameHandling.Auto,
            ObjectCreationHandling = ObjectCreationHandling.Replace,
            PreserveReferencesHandling = PreserveReferencesHandling.Objects,
            ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
            DefaultValueHandling = DefaultValueHandling.Ignore,
            ContractResolver = new KsJsonContractResolver()
        })
    {
        this.serializer.Converters.Add(new ReadOnlyObjectCollectionConverter());
        this.serializer.Converters.Add(new BusinessObjectCreationConverter());
        this.serializer.TraceWriter = new ConsoleTraceWriter();
    }

I get an exception: Cannot populate JSON object onto type 'KS.Interfaces.Core.Entities.EditableObjectCollection`1[KS.Interfaces.Core.Entities.Tests.Unit.EditableObjectCollectionTests+TestEditableObject]'. Path '$type', line 1, position 47.

in this code line of my converter:

serializer.Populate(jsonObject.CreateReader(), result);

Can any body tell me what might be the reason? I'm pretty sure that I created the correct type and with a EditableObjectBase derived object everything is fine. Only collections doesn't seem to work.

Any hints are highly appreciated, thank in advance Carsten

Upvotes: 1

Views: 1732

Answers (1)

CGR
CGR

Reputation: 101

even I haven't found a way to make my converter work there is one thing I learned during debugging the problem:

It seems that a converter should return an object that still has the same JsonToken value. In my case the JsonToken of the original object was JsonToken.Object, but for my conversion result the correct token value would be JsonToken.Array, but the reader still sees JsonToken.Object. At this point I stopped my research since I found a better way to call my custom constructor.

I wrote my own contract resolver:

   internal class BusinessBaseContractResolver : DefaultContractResolver
   {
        public BusinessBaseContractResolver()
        {
            this.DefaultMembersSearchFlags |= BindingFlags.NonPublic;    
        }

        public override JsonContract ResolveContract(Type type)
        {
            JsonContract contract = base.ResolveContract(type);
            if (typeof(BusinessObjectBase).IsAssignableFrom(type))
            {
                contract.DefaultCreator = delegate
                {
                    var businessObject = type.CreateUsingDeserializationConstructor<BusinessObjectBase>();
                    businessObject.Initialize(true);

                    return businessObject;
                };
            }

            return contract;
        }
    }

I hope this helps someone.

Best regards, Carsten

Upvotes: 2

Related Questions