I have been unable to find a reasonable implementation for JsonConvert.WriteJson that allows me to insert a JSON property when serializing specific types. All my attempts have resulted in "JsonSerializationException : Self referencing loop detected with type XXX".
A little more background on the problem I'm trying to solve: I am using JSON as a config file format, and I'm using a JsonConverter to control the type resolution, serialization, and deserialization of my configuration types. Instead of using the $type property, I want to use more meaningful JSON values that are used to resolve the correct types.
In my pared-down example, here's some JSON text:
{
"Target": "B",
"Id": "foo"
}
where the JSON property "Target": "B" is used to determine that this object should be serialized into type B. This design might not seem that compelling given the simple example, but it does make the config file format more usable.
I also want the config files to be round-trippable. I have the deserialize case working, what I can't get working is the serialize case.
The root of my problem is that I can't find an implementation of JsonConverter.WriteJson that uses the standard JSON serialization logic, and doesn't throw a "Self referencing loop" exception. Here's my implementation:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());
//BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
// Same error occurs whether I use the serializer parameter or a separate serializer.
JObject jo = JObject.FromObject(value, serializer);
if (typeHintProperty != null)
{
jo.AddFirst(typeHintProperty);
}
writer.WriteToken(jo.CreateReader());
}
The seems to me to be a bug in Json.NET, because there should be a way to do this. Unfortunately all the examples of JsonConverter.WriteJson that I've come across (eg Custom conversion of specific objects in JSON.NET) only provide custom serialization of a specific class, using the JsonWriter methods to write out individual objects and properties.
Here's the complete code for an xunit test that exhibits my problem (or see it here )
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Xunit;
public class A
{
public string Id { get; set; }
public A Child { get; set; }
}
public class B : A {}
public class C : A {}
/// <summary>
/// Shows the problem I'm having serializing classes with Json.
/// </summary>
public sealed class JsonTypeConverterProblem
{
[Fact]
public void ShowSerializationBug()
{
A a = new B()
{
Id = "foo",
Child = new C() { Id = "bar" }
};
JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
jsonSettings.ContractResolver = new TypeHintContractResolver();
string json = JsonConvert.SerializeObject(a, Formatting.Indented, jsonSettings);
Console.WriteLine(json);
Assert.Contains(@"""Target"": ""B""", json);
Assert.Contains(@"""Is"": ""C""", json);
}
[Fact]
public void DeserializationWorks()
{
string json =
@"{
""Target"": ""B"",
""Id"": ""foo"",
""Child"": {
""Is"": ""C"",
""Id"": ""bar"",
}
}";
JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
jsonSettings.ContractResolver = new TypeHintContractResolver();
A a = JsonConvert.DeserializeObject<A>(json, jsonSettings);
Assert.IsType<B>(a);
Assert.IsType<C>(a.Child);
}
}
public class TypeHintContractResolver : DefaultContractResolver
{
public override JsonContract ResolveContract(Type type)
{
JsonContract contract = base.ResolveContract(type);
if ((contract is JsonObjectContract)
&& ((type == typeof(A)) || (type == typeof(B))) ) // In the real implementation, this is checking against a registry of types
{
contract.Converter = new TypeHintJsonConverter(type);
}
return contract;
}
}
public class TypeHintJsonConverter : JsonConverter
{
private readonly Type _declaredType;
public TypeHintJsonConverter(Type declaredType)
{
_declaredType = declaredType;
}
public override bool CanConvert(Type objectType)
{
return objectType == _declaredType;
}
// The real implementation of the next 2 methods uses reflection on concrete types to determine the declaredType hint.
// TypeFromTypeHint and TypeHintPropertyForType are the inverse of each other.
private Type TypeFromTypeHint(JObject jo)
{
if (new JValue("B").Equals(jo["Target"]))
{
return typeof(B);
}
else if (new JValue("A").Equals(jo["Hint"]))
{
return typeof(A);
}
else if (new JValue("C").Equals(jo["Is"]))
{
return typeof(C);
}
else
{
throw new ArgumentException("Type not recognized from JSON");
}
}
private JProperty TypeHintPropertyForType(Type type)
{
if (type == typeof(A))
{
return new JProperty("Hint", "A");
}
else if (type == typeof(B))
{
return new JProperty("Target", "B");
}
else if (type == typeof(C))
{
return new JProperty("Is", "C");
}
else
{
return null;
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (! CanConvert(objectType))
{
throw new InvalidOperationException("Can't convert declaredType " + objectType + "; expected " + _declaredType);
}
// Load JObject from stream. Turns out we're also called for null arrays of our objects,
// so handle a null by returning one.
var jToken = JToken.Load(reader);
if (jToken.Type == JTokenType.Null)
return null;
if (jToken.Type != JTokenType.Object)
{
throw new InvalidOperationException("Json: expected " + _declaredType + "; got " + jToken.Type);
}
JObject jObject = (JObject) jToken;
// Select the declaredType based on TypeHint
Type deserializingType = TypeFromTypeHint(jObject);
var target = Activator.CreateInstance(deserializingType);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
public override bool CanWrite
{
get { return true; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());
//BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
// Same error occurs whether I use the serializer parameter or a separate serializer.
JObject jo = JObject.FromObject(value, serializer);
if (typeHintProperty != null)
{
jo.AddFirst(typeHintProperty);
}
writer.WriteToken(jo.CreateReader());
}
}