Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON .Net not respecting PreserveReferencesHandling on Deserialization

Tags:

c#

json.net

I have doubly linked list that I am trying to deserialise.

My scenario closely relates to this SO: Doubly Linked List to JSON

I have the following JSON settings:

_jsonSettings = new JsonSerializerSettings() 
{ 
    TypeNameHandling = TypeNameHandling.Auto, 
    ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
    PreserveReferencesHandling = PreserveReferencesHandling.Objects,
    ObjectCreationHandling = ObjectCreationHandling.Auto
};

When I look at the serialised output, it appears correct, and the references between nodes are properly represented.

When the data is deserialised, the Parent properties in the Child objects are null, even though they are populated with $ref correctly.

Below is a sample of the JSON (trimmed for readability)

In the process of typing this question - I may have seen the source of the trouble...

The objects in the "Children" array property do not have $type attributes.

This could be because the Children and Parent properties are of generic type T.

Note that the actual type being serialised is a derived class of TemplateDataLinkedListBase

public class TemplateDataQueryElement : TemplateDataLinkedListBase<TemplateDataQueryElement>

Here is an excerpt of the base class:

public class TemplateDataLinkedListBase<T> where T : TemplateDataLinkedListBase<T>
{
    [JsonProperty(TypeNameHandling = TypeNameHandling.Objects)]
    public T Parent { get; set; }

    [JsonProperty(TypeNameHandling=TypeNameHandling.Objects)]
    public List<T> Children { get; set; }
}

How can I deserialise this JSON in such a way that the Parent property is not null and contains a reference to the parent object?

    {
    "$id": "9",
    "$type": "Contracts.Models.TemplateDataQueryElement, Contracts",
    "Query": null,
    "Parent": null,
    "Children": [
      {
        "$id": "11",
        "Query": null,
        "Parent": {
          "$ref": "9"
        },
        "Children": [
          {
            "$id": "13",
            "Query": null,
            "Parent": {
              "$ref": "11"
            },
            "Children": [],
            "EntityName": "Widgets",
            "Fields": [
              "Id"
            ],
            "Key": ""
          },

Here are PasteBin links to the relevant code:

http://pastebin.com/i1jxVGG3 http://pastebin.com/T1xqEWW2 http://pastebin.com/ha42SeF7 http://pastebin.com/cezwZqx6 http://pastebin.com/uFbTbUZe http://pastebin.com/sRhNQgzh

like image 613
RobD Avatar asked Sep 15 '14 17:09

RobD


1 Answers

Here is what I tried and worked fine:

The classes

public class TemplateDataLinkedListBase<T> where T : TemplateDataLinkedListBase<T>
{
    [JsonProperty(TypeNameHandling = TypeNameHandling.Objects)]
    public T Parent { get; set; }

    [JsonProperty(TypeNameHandling = TypeNameHandling.Objects)]
    public List<T> Children { get; set; }
}

public class TemplateDataQueryElement : TemplateDataLinkedListBase<TemplateDataQueryElement>
{
    public string Query { get; set; }

    public TemplateDataQueryElement()
    {
        Children = new List<TemplateDataQueryElement>();
    }
}

Initialization

var childLowest = new TemplateDataQueryElement
{
    Query = "Lowest"
};

var childMiddle = new TemplateDataQueryElement
{
    Query = "Middle",
    Children = new List<TemplateDataQueryElement>
    {
        childLowest
    }
};

childLowest.Parent = childMiddle;

var parent = new TemplateDataQueryElement
{
    Query = "Parent",
    Children = new List<TemplateDataQueryElement>
    {
        childMiddle
    }
};

childMiddle.Parent = parent;

Serialization settings

var _jsonSettings = new JsonSerializerSettings()
{
    TypeNameHandling = TypeNameHandling.Auto,
    ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
    PreserveReferencesHandling = PreserveReferencesHandling.Objects,
    ObjectCreationHandling = ObjectCreationHandling.Auto
};

Serialization

var serializedStr = JsonConvert.SerializeObject(parent, Formatting.Indented, _jsonSettings);

The serialized json looks like this:

{
  "$id": "1",
  "Query": "Parent",
  "Parent": null,
  "Children": [
    {
      "$id": "2",
      "Query": "Middle",
      "Parent": {
        "$ref": "1"
      },
      "Children": [
        {
          "$id": "3",
          "Query": "Lowest",
          "Parent": {
            "$ref": "2"
          },
          "Children": []
        }
      ]
    }
  ]
}

Deserialization

var deserializedStructure = JsonConvert.DeserializeObject<TemplateDataQueryElement>(serializedStr, _jsonSettings);

The references in the deserializedStructure are preserved correctly.

Demo https://dotnetfiddle.net/j1Qhu6

UPDATE 1

The reason my example works, and the code you posted in the additional links doesn't is because my classes contain default constructor, and yours don't. Analyzing your classes, adding a default constructor to them, it won't break the functionality and the deserialization will be successful with Parent property initialized correctly. So what you basically need to do is add a default constructor to both classes:

public class TemplateDataLinkedListBase<T> where T : TemplateDataLinkedListBase<T>
{
    [JsonProperty(TypeNameHandling = TypeNameHandling.Objects)]
    public T Parent { get; set; }

    [JsonProperty(TypeNameHandling=TypeNameHandling.Objects)]
    public List<T> Children { get; set; }
    public string EntityName { get; set; }
    public HashSet<string> Fields { get; set; }

    public string Key { get { return getKey(); } }


    public TemplateDataLinkedListBase()
    {
        Children = new List<T>();
        Fields = new HashSet<string>();
    }

    public TemplateDataLinkedListBase(string entityName)
    {
        EntityName = entityName;
        Children = new List<T>();
        Fields = new HashSet<string>();
    }

    private string getKey()
    {
        List<string> keys = new List<string>();
        keys.Add(this.EntityName);
        getParentKeys(ref keys, this);
        keys.Reverse();
        return string.Join(".", keys);

    }

    private void getParentKeys(ref List<string> keys, TemplateDataLinkedListBase<T> element)
    {
        if (element.Parent != null)
        {
            keys.Add(element.Parent.EntityName);
            getParentKeys(ref keys, element.Parent);
        }
    }

    public T AddChild(T child)
    {
        child.Parent = (T)this;
        Children.Add(child);
        return (T)this;
    }

    public T AddChildren(List<T> children)
    {
        foreach (var child in children)
        {
            child.Parent = (T)this;
        }
        Children.AddRange(children);
        return (T)this;
    }

    public void AddFields(IEnumerable<string> fields)
    {
        foreach (var field in fields)
            this.Fields.Add(field);
    }

    public TemplateDataLinkedListBase<T> Find(string searchkey)
    {
        if (this.Key == searchkey)
        {
            return this;
        }
        else
        {
            foreach (var child in Children)
            {
                if (child.Key == searchkey)
                {
                    return child;
                }
                else
                {
                    var childResult = child.Find(searchkey);
                    if (childResult != null) return childResult;
                }
            }
        }
        return null;
    }
}

public class TemplateDataQueryElement : TemplateDataLinkedListBase<TemplateDataQueryElement>, ITemplateDataQueryElement
{
    public string TemplateModelName { get; set; }
    public string RecordId { get; set; }
    public string ParentForeignKeyName { get; set; }
    public string Query { get; set; }
    public dynamic ObjectData { get; set; }
    public ITemplateDataParseResult ParseResult { get; set; }


    public TemplateDataQueryElement() : base()
    {
        Fields.Add("Id"); //Always retrieve Id's
        ObjectData = new ExpandoObject();
    }

    public TemplateDataQueryElement(string entityName)
        : base(entityName)
    {
        Fields.Add("Id"); //Always retrieve Id's
        ObjectData = new ExpandoObject();
    }

    public override string ToString()
    {
        return string.Format("{0}: {1}", EntityName, Query);
    }
}

The EntityName property which you set through your constructor, will be deserialized correctly, since it is a public property.

like image 90
Ilija Dimov Avatar answered Oct 21 '22 20:10

Ilija Dimov