3

I have a JSON input whose objects are all derived from a base class along the lines of this:

public abstract class Base
{
    public Base Parent { get; set; }
}

I'm trying to create a CustomCreationConverter to set the Parent property of each object to the parent node in the JSON input using ReadJson (except for the root node, of course). Is this possible? I'd rather not have to traverse the objects after creation to set the Parent property.

Example Time!

Say I have this input JSON:

{
  "Name": "Joe",
  "Children": [
    { "Name": "Sam", "FavouriteToy": "Car" },
    { "Name": "Tom", "FavouriteToy": "Gun" },
  ]
}

I have the following two classes:

public class Person
{
    public Person Parent { get; set; }
    public string Name { get; set; }
    public List<Child> Children { get; set; }
}

public class Child : Person
{
    public string FavouriteToy { get; set; }
}

The Name and FavouriteToy properties deserialise fine, but I want to have the Parent property of any Person object set to, as you'd expect, the actual parent object within the JSON input (presumably using a JsonConverter). The best I've been able to implement so far is recursively traversing each object after deserialisation and setting the Parent property that way.

P.S.

I want to point out that I know I'm able to do this with references inside the JSON itself, but I'd rather avoid that.

Not a duplicate :(

That question refers to creating an instance of the correct derived class, the issue I'm having is finding a way to get context during the deserialisation of the objects. I'm trying to use a JsonConverter's ReadJson method to set a property of a deserialised object to refer to another object within the same JSON input, without using $refs.

Kyle H
  • 31
  • 1
  • 5
  • possible duplicate of [How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?](http://stackoverflow.com/questions/8030538/how-to-implement-custom-jsonconverter-in-json-net-to-deserialize-a-list-of-base) – Andrew Savinykh Jun 11 '15 at 04:50
  • Edited the answer to explain why: That question refers to creating an instance of the correct derived class, the issue I'm having is finding a way to get context during the deserialisation of the objects. I'm trying to use a JsonConverter's ReadJson method to set a property of a deserialised object to refer to another object within the same JSON input, *without* using `$ref`s. – Kyle H Jun 11 '15 at 05:09
  • Your question if confusing. First you state that all your objects are derived from `Base` and then you go ahead and give an example of two objects none of which is derived from `Base`. With this this self-contradicting question it is really difficult to understand what you are actually trying to achieve, so I can only guess. Can you help me and clarify it a bit? – Andrew Savinykh Jun 11 '15 at 07:47
  • @zespri Sorry, the two snippets were separate examples but I don't think the idea is that hard to grasp. Focusing only on the example section, I want to, somehow, have my program set the `Parent` property of *any* `Person` object to a reference to the actual deserialised parent object in the JSON input. – Kyle H Jun 11 '15 at 08:49
  • In response to your answer below, I said in the question that I don't want to have to traverse the tree and set the Parent property manually (that's what I'm already doing) because as the amount of objects I have gets bigger and they contain more collections of the base class it will become unreasonably cumbersome – Kyle H Jun 11 '15 at 08:51
  • Sorry, why are you talking about tree traversal in my answer? Is there anything there that suggests that? – Andrew Savinykh Jun 11 '15 at 08:55
  • To put it simply, for every child you need to put a link to the parent. That's what is being demonstrated. You are also doing that (in my answer) as the JSON is being parsed, that is you don't need an additional pass. It does not get much more efficient than that. Unless you have some other constraints that you are not mentioning. – Andrew Savinykh Jun 11 '15 at 09:00

3 Answers3

4

My best guess is that you are after something like this:

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

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    object value = Activator.CreateInstance(objectType);
    serializer.Populate(reader, value);

    Person p  = value as Person;
    if (p.Children != null)
    {
        foreach (Child child in p.Children)
        {
            child.Parent = p;
        }
    }
    return value;
}

Note: If you are de-serializing this class a lot (like de-serializing model from http request in a web application) you'll get better performance creating objects with a pre-compiled factory, rather than with object activator:

object value = serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();

Note that it is not possible to get access to a parent, because parent objects are always created after their children. That is you need to read the json that the object consists of to the end to be able fully construct the object and by the time you've read the last bracket of an object, you've already read all its children. When a child parsed there is no parent yet to get the reference of.

Andrew Savinykh
  • 25,351
  • 17
  • 103
  • 158
1

I ran into a similar dilemma. However, in my particular scenario, I really needed property Parent to be readonly or, at least, private set. For that reason, @Andrew Savinykh's solution, despite being very good, wasn't enough for me. Thus, I finally ended merging different approaches together until I reached a possible "solution" or workaround.


JsonContext

To start with, I noticed that JsonSerializer provides a public readonly Context property, which may be used to share data between instances and converters involved in the same deserialization proccess. Taking advantage of this, I implemented my own context class as follows:

public class JsonContext : Dictionary<string, object>
{
    public void AddUniqueRef(object instance)
    {
        Add(instance.GetType().Name, instance);
    }

    public bool RemoveUniqueRef(object instance)
    {
        return Remove(instance.GetType().Name);
    }

    public T GetUniqueRef<T>()
    {
        return (T)GetUniqueRef(typeof(T));
    }

    public bool TryGetUniqueRef<T>(out T value)
    {
        bool result = TryGetUniqueRef(typeof(T), out object obj);
        value = (T)obj;
        return result;
    }

    public object GetUniqueRef(Type type)
    {
        return this[type.Name];
    }

    public bool TryGetUniqueRef(Type type, out object value)
    {
        return TryGetValue(type.Name, out value);
    }
}

Next, I needed to add an instance of my JsonContext to my JsonSerializerSetttings:

var settings = new JsonSerializerSettings
    {       
        Context = new StreamingContext(StreamingContextStates.Other, new JsonContext()),
        // More settings here [...]
    };

_serializer = JsonSerializer.CreateDefault(settings);

OnDeserializing / OnDeserialized

I tried to use this context at OnDeserializing and OnDeserialized callbacks, but, as @Andrew Savinykh states, they are called in the following order:

  1. Child.OnDeserializing
  2. Child.OnDeserialized
  3. Person.OnDeserializing
  4. Person.OnDeserialized

EDIT: After finishing my initial implementation (see Solution 2) I noticed that, while using any kind of *CreationConverter, the above order is modified as follows:

  1. Person.OnDeserializing
  2. Child.OnDeserializing
  3. Child.OnDeserialized
  4. Person.OnDeserialized

I am not really sure of the reason behind this. It might be related to the fact that JsonSerializer normally uses Deserialize which wraps, between deserialization callbacks, instance creation and population, from bottom to top, of the object composition tree. By contrast, while using a CustomCreationConverter, the serializer delegates the instantiation to our Create method and then it might be only executing Populate in the second stacked order.

This stacked callback calling order is very convenient if we are looking for a simpler solution (see Solution 1). Taking advantage of this edition, I am adding this new approach in first place below (Solution 1) and the original and more complex one at the end (Solution 2).


Solution 1. Serialization Callbacks

Compared to Solution 2, this may be a simpler and more elegant approach. Nevertheless, it does not support initializing readonly members via constructor. If that is your case, please refer to Solution 2.

A requirement for this implementation, as I stated above, is a CustomCreationConverter to force callbacks to be called in a convenient order. E.g, we could use the following PersonConverter for both Person and Child.

public sealed class PersonConverter : CustomCreationConverter<Person>
{
    /// <inheritdoc />
    public override Person Create(Type objectType)
    {
        return (Person)Activator.CreateInstance(objectType);
    }
}

Then, we only have to access our JsonContext on serialization callbacks to share the Person Parent property.

public class Person
{
    public Person Parent { get; set; }
    public string Name { get; set; }
    public List<Child> Children { get; set; }
    
    [OnDeserializing]
    private void OnDeserializing(StreamingContext context)
    {
        ((JsonContext)context.Context).AddUniqueRef(this);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        ((JsonContext)context.Context).RemoveUniqueRef(this);
    }
}

public class Child : Person
{
    public string FavouriteToy { get; set; }
   
    [OnDeserializing]
    private void OnDeserializing(StreamingContext context)
    {
        Parent = ((JsonContext)context.Context).GetUniqueRef<Person>();
    }
}

Solution 2. JObjectCreationConverter

Here it is my initial solution. It does support initializing readonly members via parameterized constructor. It could be combined with Solution 1, moving JsonContext usage to serialization callbacks.

In my specific scenario, Person class lacks a parameterless constructor because it needs to initialize some readonly members (i.e. Parent). To achieve this, we need our own JsonConverter class, totally based on CustomCreationConverter implementation, using an abstract T Create method with two new arguments: JsonSerializer, in order to provide access to my JsonContext, and JObject, to pre-read some values from reader.

/// <summary>
///     Creates a custom object.
/// </summary>
/// <typeparam name="T">The object type to convert.</typeparam>
public abstract class JObjectCreationConverter<T> : JsonConverter
{
    #region Public Overrides JsonConverter

    /// <summary>
    ///     Gets a value indicating whether this <see cref="JsonConverter" /> can write JSON.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this <see cref="JsonConverter" /> can write JSON; otherwise, <c>false</c>.
    /// </value>
    public override bool CanWrite => false;

    /// <summary>
    ///     Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter" /> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <exception cref="NotSupportedException">JObjectCreationConverter should only be used while deserializing.</exception>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException($"{nameof(JObjectCreationConverter<T>)} should only be used while deserializing.");
    }

    /// <summary>
    ///     Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader" /> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing value of object being read.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    /// <exception cref="JsonSerializationException">No object created.</exception>
    /// <exception cref="JsonReaderException"><paramref name="reader" /> is not valid JSON.</exception>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        T value = Create(jObject, objectType, serializer);
        if (value == null)
        {
            throw new JsonSerializationException("No object created.");
        }        
      
        using (JsonReader jObjectReader = jObject.CreateReader(reader))
        {
            serializer.Populate(jObjectReader, value);
        }

        return value;
    }

    /// <summary>
    ///     Determines whether this instance can convert the specified object type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    ///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
    /// </returns>
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    #endregion

    #region Protected Methods

    /// <summary>
    ///     Creates an object which will then be populated by the serializer.
    /// </summary>
    /// <param name="jObject"><see cref="JObject" /> instance to browse the JSON object being deserialized</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The created object.</returns>
    protected abstract T Create(JObject jObject, Type objectType, JsonSerializer serializer);

    #endregion
}

NOTE: CreateReader is a custom extension method that calls the default and parameterless CreaterReader and then imports all settings from the original reader. See @Alain's response for more details.

Finally, if we apply this solution to the given (and customized) example:

//{
//    "name": "Joe",
//    "children": [
//    {
//        "name": "Sam",
//        "favouriteToy": "Car",
//        "children": []
//    },
//    {
//        "name": "Tom",
//        "favouriteToy": "Gun",
//        "children": []
//    }
//    ]
//}

public class Person 
{
    public              string             Name     { get; }
    [JsonIgnore] public Person             Parent   { get; }
    [JsonIgnore] public IEnumerable<Child> Children => _children;

    public Person(string name, Person parent = null)
    {
        _children = new List<Child>();
        Name      = name;
        Parent    = parent;
    }

    [JsonProperty("children", Order = 10)] private readonly IList<Child> _children;
}

public sealed class Child : Person
{
    public string FavouriteToy { get; set; }

    public Child(Person parent, string name, string favouriteToy = null) : base(name, parent)
    {
        FavouriteToy = favouriteToy;
    }
}

We only have to add the following JObjectCreationConverters:

public sealed class PersonConverter : JObjectCreationConverter<Person>
{
    #region Public Overrides JObjectCreationConverter<Person>

    /// <inheritdoc />
    /// <exception cref="JsonSerializationException">No object created.</exception>
    /// <exception cref="JsonReaderException"><paramref name="reader" /> is not valid JSON.</exception>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        object result = base.ReadJson(reader, objectType, existingValue, serializer);
        ((JsonContext)serializer.Context.Context).RemoveUniqueRef(result);
        return result;
    }

    #endregion

    #region Protected Overrides JObjectCreationConverter<Person>

    /// <inheritdoc />
    protected override Person Create(JObject jObject, Type objectType, JsonSerializer serializer)
    {
        var person = new Person((string)jObject["name"]);
        ((JsonContext)serializer.Context.Context).AddUniqueRef(person);
        return person;
    }

    public override bool CanConvert(Type objectType)
    {
        // Overridden with a more restrictive condition to avoid this converter from being used by child classes.
        return objectType == typeof(Person);
    }
    #endregion
}

public sealed class ChildConverter : JObjectCreationConverter<Child>
{
    #region Protected Overrides JObjectCreationConverter<Child>

    /// <inheritdoc />
    protected override Child Create(JObject jObject, Type objectType, JsonSerializer serializer)
    {
        var parent = ((JsonContext)serializer.Context.Context).GetUniqueRef<Person>();
        return new Child(parent, (string)jObject["name"]);
    }

    /// <inheritdoc />
    public override bool CanConvert(Type objectType)
    {
        // Overridden with a more restrictive condition.
        return objectType == typeof(Child);
    }

    #endregion
}

Bonus Track. ContextCreationConverter

public class ContextCreationConverter : JsonConverter
{
    #region Public Overrides JsonConverter

    /// <summary>
    ///     Gets a value indicating whether this <see cref="JsonConverter" /> can write JSON.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this <see cref="JsonConverter" /> can write JSON; otherwise, <c>false</c>.
    /// </value>
    public override sealed bool CanWrite => false;

    /// <summary>
    ///     Determines whether this instance can convert the specified object type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    ///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
    /// </returns>
    public override sealed bool CanConvert(Type objectType)
    {
        return false;
    }

    /// <summary>
    ///     Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter" /> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <exception cref="NotSupportedException">ContextCreationConverter should only be used while deserializing.</exception>
    public override sealed void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException($"{nameof(ContextCreationConverter)} should only be used while deserializing.");
    }

    /// <summary>
    ///     Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader" /> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing value of object being read.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    /// <exception cref="JsonReaderException"><paramref name="reader" /> is not valid JSON.</exception>
    /// <exception cref="JsonSerializationException">No object created.</exception>
    public override sealed object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        // Load JObject from stream
        JObject jObject = JObject.Load(reader);
        object  value   = Create(jObject, objectType, serializer);

        using (JsonReader jObjectReader = jObject.CreateReader(reader))
        {
            serializer.Populate(jObjectReader, value);
        }

        return value;
    }

    #endregion

    #region Protected Methods

    protected virtual object GetCreatorArg(Type type, string name, JObject jObject, JsonSerializer serializer)
    {
        JsonContext context = (JsonContext)serializer.Context.Context;

        if (context.TryGetUniqueRef(type, out object value))
        {
            return value;
        }

        if (context.TryGetValue(name, out value))
        {
            return value;
        }

        if (jObject.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out JToken jToken))
        {
            return jToken.ToObject(type, serializer);
        }

        if (type.IsValueType)
        {
            return Activator.CreateInstance(type);
        }

        return null;
    }

    #endregion

    #region Private Methods

    /// <summary>
    ///     Creates a instance of the <paramref name="objectType" />
    /// </summary>
    /// <param name="jObject">
    ///     The JSON Object to read from
    /// </param>
    /// <param name="objectType">
    ///     Type of the object to create.
    /// </param>
    /// <param name="serializer">
    ///     The calling serializer.
    /// </param>
    /// <returns>
    ///     A new instance of the <paramref name="objectType" />
    /// </returns>
    /// <exception cref="JsonSerializationException">
    ///     Could not found a constructor with the expected signature
    /// </exception>
    private object Create(JObject jObject, Type objectType, JsonSerializer serializer)
    {
        JsonObjectContract contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);

        ObjectConstructor<object> creator = contract.OverrideCreator ?? GetParameterizedConstructor(objectType).Invoke;
        if (creator == null)
        {
            throw new JsonSerializationException($"Could not found a constructor with the expected signature {GetCreatorSignature(contract)}");
        }

        object[] args = GetCreatorArgs(contract.CreatorParameters, jObject, serializer);
        return creator(args);
    }

    private object[] GetCreatorArgs(JsonPropertyCollection parameters, JObject jObject, JsonSerializer serializer)
    {
        var result = new object[parameters.Count];

        for (var i = 0; i < result.Length; ++i)
        {
            result[i] = GetCreatorArg(parameters[i].PropertyType, parameters[i].PropertyName, jObject, serializer);
        }

        return result;
    }

    private ConstructorInfo GetParameterizedConstructor(Type objectType)
    {
        var constructors = objectType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
        return constructors.Length == 1 ? constructors[0] : null;
    }

    private string GetCreatorSignature(JsonObjectContract contract)
    {
        StringBuilder sb = contract.CreatorParameters
                                   .Aggregate(new StringBuilder("("), (s, p) => s.AppendFormat("{0} {1}, ", p.PropertyType.Name, p.PropertyName));
        return sb.Replace(", ", ")", sb.Length - 2, 2).ToString();
    }

    #endregion
}

USAGE:

// For Person we could use any other CustomCreationConverter.
// The only purpose is to achievee the stacked calling order for serialization callbacks.
[JsonConverter(typeof(ContextCreationConverter))]
public class Person
{
    public              string             Name     { get; }
    [JsonIgnore] public IEnumerable<Child> Children => _children;
    [JsonIgnore] public Person             Parent   { get; }

    public Person(string name, Person parent = null)
    {
        _children = new List<Child>();
        Name      = name;
        Parent    = parent;
    }   

    [OnDeserializing]
    private void OnDeserializing(StreamingContext context)
    {
        ((JsonContext)context.Context).AddUniqueRef(this);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        ((JsonContext)context.Context).RemoveUniqueRef(this);
    }

    [JsonProperty("children", Order = 10)] private readonly IList<Child> _children;
}

[JsonConverter(typeof(ContextCreationConverter))]
public sealed class Child : Person
{
    [JsonProperty(Order = 5)] public string FavouriteToy { get; set; }

    public Child(Person parent, string name, string favouriteToy = null) : base(name, parent)
    {
        FavouriteToy = favouriteToy;
    }
}
fibriZo raZiel
  • 894
  • 11
  • 10
0

I create an example:

        public class Base
        {
            //This is JSON object's attribute.
            public string CPU {get; set;}
            public string PSU { get; set; }
            public List<string> Drives { get; set; }
            public string price { get; set; }
            //and others...
        }

        public class newBase : Base
        {
            ////same
            //public string CPU { get; set; }
            //public string PSU { get; set; }
            //public List<string> Drives { get; set; }

            //convert to new type
            public decimal price { get; set; }  //to other type you want

            //Added new item
            public string from { get; set; }
        }

    public class ConvertBase : CustomCreationConverter<Base>
    {
        public override Base Create(Type objectType)
        {
            return new newBase();
        }
    }

    static void Main(string[] args)
    {
        //from http://www.newtonsoft.com/json/help/html/ReadJsonWithJsonTextReader.htm (creadit) + modify by me
        string SimulateJsonInput = @"{'CPU': 'Intel', 'PSU': '500W', 'Drives': ['DVD read/writer', '500 gigabyte hard drive','200 gigabype hard drive'], 'price' : '3000', 'from': 'Asus'}";

        JsonSerializer serializer = new JsonSerializer();
        Base Object = JsonConvert.DeserializeObject<Base>(SimulateJsonInput);
        Base converted = JsonConvert.DeserializeObject<Base>(SimulateJsonInput, new ConvertBase());
        newBase newObject = (newBase)converted;

        //Console.Write(Object.from);
        Console.WriteLine("Newly converted atrribute type attribute =" + " " + newObject.price.GetType());
        Console.WriteLine("Newly added attribute =" + " " + newObject.from);
        Console.Read();   
    }

hope this helps. Support link: Json .net documentation

Robert Ling
  • 33
  • 1
  • 8
  • Sorry, I'm not sure how this applies to my question? To clarify, I want to set a property on a specific type of deserialised object to a reference to said object's parent object in the JSON input. – Kyle H Jun 11 '15 at 03:37