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:
Child.OnDeserializing
Child.OnDeserialized
Person.OnDeserializing
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:
Person.OnDeserializing
Child.OnDeserializing
Child.OnDeserialized
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 JObjectCreationConverter
s:
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;
}
}