I'm aware that there are many related questions about recursive serialising in Json.NET, but I don't think any of them really suit me or reflect my problem.
I'm currently experimenting with a serialisation system in C#, Unity, that uses Json.NET custom converters. The aim is to create a system that can support reference serialisation and deserialisation.
Obviously, there are two types of serialisation. The first is by value, where I'd want to write the defined data of a given object (thus serialising the fact that a Pirate Ship is called "The Royal Hippo", travels at 20f km/h, and flies a red flag). Elsewhere, I may be referencing the Royal Hippo, perhaps even from another Pirate Ship (what if the ships tracked their nemesis-ship?); in these cases I'd want to serialise as a reference. I can of course use a string, or a hash code, or even Json.NET's native referencing system, to accomplish this.
My plan is to use hash codes for serialising a reference. So I need two custom converters to accomplish this: one to recognise an object that CAN be referenced, and thus needs its hash-code written into its json file, and the other to recognise when an object is being REFERENCED instead of defined, thus to serialise it as ONLY its hash code.
A Json file of two ships that have each other as a nemesis should look like this:
{
"pirateShips": [
{
"HashCode": 123
"name": "The Seagull"
"flagCol": {
"a": 1.0,
"r": 1.0,
"g": 0.921568632,
"b": 0.0156862754
},
"speed": 20.0,
"nemesis": 456,
},
{
"HashCode": 456
"name": "The Beagle"
"flagCol": {
"a": 1.0,
"r": 0.0,
"g": 0.0,
"b": 1.0
},
"speed": 30.0,
"nemesis": 123
}
]
}
To specify which classes need to include a hash-code for deserialising purposes, I've created a simple attribute called Referable and made a custom converter - HashJConverter - that targets it.
/// <summary>
/// A converter specifically for any object that is decorated with the Referable attribute, indicating that it needs to have its hashcode serialised with it.
/// </summary>
public class HashJConverter : JsonConverter
{
// Does the object have the Referable attribute?
public override bool CanConvert(Type objectType)
{
return objectType.HasAttribute<ReferableAttribute>();
}
// When serialising, put the hashcode into the Json object.
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JObject jObj = JObject.FromObject(value, serializer); // First, get the JObject.
jObj.Add("HashCode", value.GetHashCode()); // Then add a one-off hashcode field.
jObj.WriteTo(writer);
}
// Worry about this later ...
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return objectType.HasAttribute<ReferableAttribute>();
}
}
And the PirateShip class, decorated with the Referable attribute:
/// <summary>
/// This is a dummy class used to describe a pirate-ship as a serialisation example.
/// </summary>
[Referable]
public class PirateShip
{
// Serialise these normally.
[SerializeField]
public string name = "";
[SerializeField]
public Color flagCol = new Color();
[SerializeField]
public float speed = 10f;
// This is a reference, not a definition. The RefjConverter ensures that this reference is serialised as its hashcode.
[SerializeField]
[JsonConverter(typeof(RefJConverter))]
public PirateShip nemesis = null;
public PirateShip(Color _flagCol, float _speed)
{
flagCol = _flagCol;
speed = _speed;
}
}
Finally, the definition of the RefJConverter:
/// <summary>
/// The custom converter used to handle references. A reference should be serialised as a hashcode.
/// </summary>
public class RefJConverter : JsonConverter
{
// This Converter is invoked via an explicit Json.NET attribute, so simply return always true for CanConvert.
public override bool CanConvert(Type objectType)
{
return true;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Write the hashcode.
if(value.GetType().HasAttribute<ReferableAttribute>())
{
writer.WriteValue(value.GetHashCode());
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
}
}
Now, you may notice that both custom converters, in this case, would be viable converters for the PirateShip.nemesis field. But according to the Json.NET documentation,
The priority of which JsonConverter is used is member attribute, then class attribute, and finally any converters passed to the JsonSerializer.
So my member attribute [JsonConverter(typeof(RefJConverter))] should be overriding the Referable attribute.
Instead, HashJConverter calls the serializer when JObject.FromObject is called. This reads the members of PirateShip, one of which is itself a PirateShip. Instead of converting this using the RefJConverter, it once again uses HashJConverter, causing infinite recursion and a stack overflow. Unity crashes immediately, unless I'm doing a unit test, in which case it just freezes.
Does anyone know why this is happening? Is it something about the context under which I'm calling JObject.FromObject? Is there a way to solve this issue, or an approach that avoids it entirely?