1

I am attempting to use Newtonsoft.json DefaultContractResolver to output json. When the ContractResolver finds a class of type IVariant, it sets the output based upon the IVariant's "TypeName"

Without my ContractResolver code the json would look something like this:

{
  "Data" :
  {
    "Aim" :
      {
        "Joystick" :
        {
          "Test" : "Test"
        }, 
        "Button" :
        {
          "Test" : "Test2"
        }
      }
  }
}

and with my resolver, It replaces "Aim" with either Joystick, or Button, and look like this:

{
  "Data" :
  {
    "Button" :
    {
      "Test" : "Test2"
    }
  }
}

Below is the full code I used.

  public class JsonTester
  {
    public void Test()
    {
      var d = new Outer();

      var json = JsonConvert.SerializeObject(d, new JsonSerializerSettings()
      {
        ContractResolver = new JsonContractResolver()
      });
    }
  }

  public interface IVariant
  {
    object Data { get; }

    string TypeName { get; set; }
  }

  public interface IVariantData
  {
  }

  [DataContract]
  public class Variant<TEnum, TData> : IVariant where TEnum : struct where TData : class, IVariantData, new()
  {
    [DataMember]
    public TData Data { get; }

    public string TypeName { get; set; }

    object IVariant.Data => Data;

    public Variant()
    {
      Data = new TData();
    }
  }

  [DataContract]
  public sealed class VariantHolder
  {
    [DataContract]
    public class Joystick
    {
      [DataMember]
      public string Test { get; set; } = "Test";
    }

    [DataContract]
    public class Button
  {
    [DataMember]
    public string Test2 { get; set; } = "Test2";
  }

    [DataContract]
    public sealed class AimΞVariant : Variant<AimΞVariant.VariantEnum, AimΞVariant.VariantData>
  {
    public AimΞVariant()
    {
      TypeName = "Button";
    }
    public enum VariantEnum : byte
    {
      Joystick,
      Button
    }

    [DataContract]
    public sealed class VariantData : IVariantData
    {
      [DataMember]
      Joystick _Joystick = new Joystick();

      [DataMember]
      Button _Button = new Button();

      public Button Button => _Button;
      public Joystick Joystick => _Joystick;
    }
  }

    [DataMember]
    AimΞVariant Aim = new AimΞVariant();
  }

  [DataContract]
  public sealed class Outer
  {
    [DataMember]
    VariantHolder Data = new VariantHolder();
  }

  public class JsonContractResolver : DefaultContractResolver
  {
    public JsonContractResolver()
    {

    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
      return base.CreateProperty(member, memberSerialization);
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
  {
    var list = type.GetProperties().Where(x => x.GetCustomAttributes().Any(a => a.GetType() == typeof(DataMemberAttribute))).Select(p =>
    {
      var member = p.GetCustomAttribute<DataMemberAttribute>();

      return new JsonProperty()
      {
        PropertyName = p.Name,
        PropertyType = p.PropertyType,
        Readable = true,
        Writable = true,
        ValueProvider = CreateMemberValueProvider(p),
        DefaultValueHandling = DefaultValueHandling.Include
      };
    }).Concat(
    type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(x => x.GetCustomAttributes().Any(a => a.GetType() == typeof(DataMemberAttribute))).Select(p =>
    {
      var member = p.GetCustomAttribute<DataMemberAttribute>();
      return new JsonProperty()
      {
        PropertyName = p.Name,
        PropertyType = p.FieldType,
        Readable = true,
        Writable = true,
        ValueProvider = CreateMemberValueProvider(p),
        DefaultValueHandling = DefaultValueHandling.Include
      };
    })).ToList();

    return list;
  }

    protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
    {
      Type type = member.MemberType == MemberTypes.Property ? ((PropertyInfo)member).PropertyType : ((FieldInfo)member).FieldType;

      if (typeof(IVariant).IsAssignableFrom(type))
      {
        return new IVariantValueProvider(member, type);
      }

      return base.CreateMemberValueProvider(member);
    }
  }

  internal class IVariantValueProvider : IValueProvider
  {
    private MemberInfo member;
    private Type type;

    public IVariantValueProvider(MemberInfo member, Type type)
    {
      this.member = member;
      this.type = type;
    }

    public object GetValue(object target)
    {
      IVariant variant;
      
      switch (member)
      {
      case PropertyInfo p:
        variant = (IVariant)p.GetValue(target);
        break;
      case FieldInfo f:
        variant = (IVariant)f.GetValue(target);
        break;
      default:
        throw new InvalidCastException();
      }

      var dataType = variant.TypeName;

      var property = variant.Data.GetType().GetProperty(dataType);

      var value = property.GetValue(variant.Data);

      return value;
    }

    public void SetValue(object target, object value)
    {
      throw new NotImplementedException();
    }
  }

The code crashes somewhere inside JsonConvert.SeralizeObject.

Newtonsoft.Json.JsonSerializationException: 'Error getting value from 'Data' on 'Iugo.Test.VariantHolder+Button'.'

Inner Exception: InvalidCastException: Unable to cast object of type 'Button' to type 'Iugo.Test.Variant`2[Iugo.Test.VariantHolder+AimΞVariant+VariantEnum,Iugo.Test.VariantHolder+AimΞVariant+VariantData]'.
Ilan Keshet
  • 514
  • 5
  • 19
  • You've asked the [similar question](https://stackoverflow.com/questions/64473008/c-sharp-using-ivalueprovider-in-newtonsoft-json-to-change-item-in-lists-name) already – Pavel Anikhouski Oct 22 '20 at 09:56

1 Answers1

1

I was able to reproduce your InvalidCastException here: fiddle #1.

The cause of the exception is that you are setting a custom JsonProperty.ValueProvider that returns some nested object -- but you are not updating JsonProperty.PropertyType to be assignable from the type actually returned. Later, during serialization, Json.NET assumes that the value returned by the value provider is in fact compatible with the property type -- and throws an exception when this is not the case.

You will thus need to update JsonProperty.PropertyType to be consistent with the value actually returned, but since this is not actually known at compile time, you can assign the property type to be typeof(object) to indicate that any type can be returned.

A simplified version of your JsonContractResolver that does this is as follows:

public class JsonContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (typeof(IVariant).IsAssignableFrom(property.PropertyType))
        {
            property.ValueProvider = new IVariantValueProvider(member, property.PropertyType);
            property.DefaultValueHandling = DefaultValueHandling.Include;
            // The following fixes the InvalidCastException.  PropertyType has to be assignable from the type actually returned.
            // Since we don't know that at compile time, we need to set it to typeof(object).
            property.PropertyType = typeof(object);
        }
        return property;
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var list = type.GetProperties().Where(x => x.GetCustomAttributes().Any(a => a.GetType() == typeof(DataMemberAttribute))).Select(p =>
        {
            return CreateProperty(p, memberSerialization);
        }).Concat(
        type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(x => x.GetCustomAttributes().Any(a => a.GetType() == typeof(DataMemberAttribute))).Select(p =>
        {
            return CreateProperty(p, memberSerialization);
        })).ToList();

        return list;
    }
}

Demo fix here: fiddle #2.

With this fix, you will get the following JSON, which is closer to what you want, but not exactly correct:

{
  "Data": {
    "Aim": {
      "Test2": "Test2"
    }
  }
}

I.e. "Aim" has not been renamed to "Button". That's because you are modifying the value provider for the Aim property, which cannot possibly modify the name of the property itself. In fact, Json.NET has no built-in mechanism to modify a property name in runtime. For some options to do this manually, see:

dbc
  • 104,963
  • 20
  • 228
  • 340