crimbo
crimbo

Reputation: 11211

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

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());
    }

}

Upvotes: 22

Views: 13434

Answers (8)

DalSoft
DalSoft

Reputation: 11097

Ran into this problem in 2019 :)

The answer is, if you don't want an @stackoverflow Don't Forget to override:

  • bool CanWrite
  • bool CanRead

    public class DefaultJsonConverter : JsonConverter
    {
        [ThreadStatic]
        private static bool _isReading;
    
        [ThreadStatic]
        private static bool _isWriting;
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            try
            {
                _isWriting = true;
    
                Property typeHintProperty = TypeHintPropertyForType(value.GetType());
    
                var jObject = JObject.FromObject(value, serializer);
                if (typeHintProperty != null)
                {
                    jObject.AddFirst(typeHintProperty);
                }
                writer.WriteToken(jObject.CreateReader());
            }
            finally
            {
                _isWriting = false;
            }
        }
    
        public override bool CanWrite
        {
            get
            {
                if (!_isWriting)
                    return true;
    
                _isWriting = false;
    
                return false;
            }
        }
    
        public override bool CanRead
        {
            get
            {
                if (!_isReading)
                    return true;
    
                _isReading = false;
    
                return false;
            }
        }
    
        public override bool CanConvert(Type objectType)
        {
            return true;
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            try
            {
                _isReading = true;
                return serializer.Deserialize(reader, objectType);
            }
            finally
            {
                _isReading = false;
            }
        }
    }
    

Credit to: https://github.com/RicoSuter/NJsonSchema/blob/master/src/NJsonSchema/Converters/JsonInheritanceConverter.cs

Upvotes: 2

StrangeWill
StrangeWill

Reputation: 2136

Example of using a custom converter to take a property we ignore, break it down and add it's properties to it's parent object.:

public class ContextBaseSerializer : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(ContextBase).GetTypeInfo().IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var contextBase = value as ContextBase;
        var valueToken = JToken.FromObject(value, new ForcedObjectSerializer());

        if (contextBase.Properties != null)
        {
            var propertiesToken = JToken.FromObject(contextBase.Properties);
            foreach (var property in propertiesToken.Children<JProperty>())
            {
                valueToken[property.Name] = property.Value;
            }
        }

        valueToken.WriteTo(writer);
    }
}

We must override the serializer so we can specify a custom resolver:

public class ForcedObjectSerializer : JsonSerializer
{
    public ForcedObjectSerializer()
        : base()
    {
        this.ContractResolver = new ForcedObjectResolver();
    }
}

And in the custom resolver we'll trash the Converter from the JsonContract, this will force the internal serializers to use the default object serializer:

public class ForcedObjectResolver : DefaultContractResolver
{
    public override JsonContract ResolveContract(Type type)
    {
        // We're going to null the converter to force it to serialize this as a plain object.
        var contract =  base.ResolveContract(type);
        contract.Converter = null;
        return contract;
    }
}

That should get you there, or close enough. :) I use this in https://github.com/RoushTech/SegmentDotNet/ which has test cases covering this use case (including nesting our custom serialized class), details on the discussion covering that here: https://github.com/JamesNK/Newtonsoft.Json/issues/386

Upvotes: 9

Keith
Keith

Reputation: 21244

Brian's answer is great and should help the OP, but the answer has a couple of problems that others may run into, namely: 1) an overflow exception gets thrown when serializing array properties, 2) any static public properties will be emitted to JSON which you likely don't want.

Here is another version that tackles those problems:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    Type valueType = value.GetType();
    if (valueType.IsArray)
    {
        var jArray = new JArray();
        foreach (var item in (IEnumerable)value)
            jArray.Add(JToken.FromObject(item, serializer));

        jArray.WriteTo(writer);
    }
    else
    {
        JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());

        var jObj = new JObject();
        if (typeHintProperty != null)
            jo.Add(typeHintProperty);

        foreach (PropertyInfo property in valueType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            if (property.CanRead)
            {
                object propertyValue = property.GetValue(value);
                if (propertyValue != null)
                    jObj.Add(property.Name, JToken.FromObject(propertyValue, serializer));
            }
        }

        jObj.WriteTo(writer);
    }
}

Upvotes: 2

Andrew Savinykh
Andrew Savinykh

Reputation: 26280

How about this:

public class TypeHintContractResolver : DefaultContractResolver
{

  protected override IList<JsonProperty> CreateProperties(Type type,
      MemberSerialization memberSerialization)
  {
    IList<JsonProperty> result = base.CreateProperties(type, memberSerialization);
    if (type == typeof(A))
    {
      result.Add(CreateTypeHintProperty(type,"Hint", "A"));
    }
    else if (type == typeof(B))
    {
      result.Add(CreateTypeHintProperty(type,"Target", "B"));
    }
    else if (type == typeof(C))
    {
      result.Add(CreateTypeHintProperty(type,"Is", "C"));
    }
    return result;
  }

  private JsonProperty CreateTypeHintProperty(Type declaringType, string propertyName, string propertyValue)
  {
    return new JsonProperty
    {
        PropertyType = typeof (string),
        DeclaringType = declaringType,
        PropertyName = propertyName,
        ValueProvider = new TypeHintValueProvider(propertyValue),
        Readable = false,
        Writable = true
    };
  }
}

The type value provider required for that can be as simple as this:

public class TypeHintValueProvider : IValueProvider
{

  private readonly string _value;
  public TypeHintValueProvider(string value)
  {
    _value = value;
  }

  public void SetValue(object target, object value)
  {        
  }

  public object GetValue(object target)
  {
    return _value;
  }

}

Fiddle: https://dotnetfiddle.net/DRNzz8

Upvotes: 3

buzzwauld
buzzwauld

Reputation: 36

After having the same issue, and finding this and other similar questions, I found that the JsonConverter has an over-ridable property CanWrite.

Overriding this property to return false fixed this issue for me.

public override bool CanWrite
{
    get
    { 
        return false;
    }
}

Hopefully, this will help others having the same problem.

Upvotes: -2

Tomasz Pluskiewicz
Tomasz Pluskiewicz

Reputation: 3662

I've had a similar problem and here's what I do in the contract resolver

if (contract is JsonObjectContract && ShouldUseConverter(type))     
{
    if (contract.Converter is TypeHintJsonConverter)
    {
        contract.Converter = null;
    }
    else
    {
        contract.Converter = new TypeHintJsonConverter(type);
    }
}

This was the only way I found to avoid the StackOverflowException. Effectively every other call will not use the converter.

Upvotes: 1

Brian Rogers
Brian Rogers

Reputation: 129697

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

Upvotes: 16

James Newton-King
James Newton-King

Reputation: 49042

The serializer is calling into your converter which is then calling into the serializer which is calling into your converter, etc.

Either use a new instance of the serializer that doesn't have your converter with JObject.FromObject, or serialize the type's members manually.

Upvotes: 0

Related Questions