Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Json.NET, how to customize serialization to insert a JSON property

Tags:

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());     }  } 
like image 854
crimbo Avatar asked Sep 30 '14 20:09

crimbo


People also ask

Does JSON need serialization?

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.

Can JSON be serialized?

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.


1 Answers

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

like image 62
Brian Rogers Avatar answered Sep 25 '22 18:09

Brian Rogers