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()); } }
JSON is a format that encodes objects in a string. Serialization means to convert an object into that string, and deserialization is its inverse operation (convert string -> object). If you serialize this result it will generate a text with the structure and the record returned.
JSON is a format that encodes an object to a string. On the transmission of data or storing is a file, data need to be in byte strings, but as complex objects are in JSON format. Serialization converts these objects into byte strings which is JSON serialization.
Calling JObject.FromObject()
from within a converter on the same object being converted will result in a recursive loop, as you have seen. Normally the solution is to either (a) use a separate JsonSerializer instance inside the converter, or (b) serialize the properties manually, as James pointed out in his answer. Your case is a little special in that neither of these solutions really work for you: if you use a separate serializer instance that doesn't know about the converter then your child objects will not get their hint properties applied. And serializing completely manually doesn't work for a generalized solution, as you mentioned in your comments.
Fortunately, there is a middle ground. You can use a bit of reflection in your WriteJson
method to get the object properties, then delegate from there to JToken.FromObject()
. The converter will be called recursively as it should for the child properties, but not for the current object, so you don't get into trouble. One caveat with this solution: if you happen to have any [JsonProperty]
attributes applied to the classes handled by this converter (A, B and C in your example), those attributes will not be respected.
Here is the updated code for the WriteJson
method:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JProperty typeHintProperty = TypeHintPropertyForType(value.GetType()); JObject jo = new JObject(); if (typeHintProperty != null) { jo.Add(typeHintProperty); } foreach (PropertyInfo prop in value.GetType().GetProperties()) { if (prop.CanRead) { object propValue = prop.GetValue(value); if (propValue != null) { jo.Add(prop.Name, JToken.FromObject(propValue, serializer)); } } } jo.WriteTo(writer); }
Fiddle: https://dotnetfiddle.net/jQrxb8
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With