Disable type annotation for specific types with Json.NET

Viewed 412

In the process of moving away from $type annotations (in order to make the data language-independent), I'm having some issues understanding the priority of the various TypeNameHandling annotations and type contract.

During the transiton, both the new types and old types will be contained in the same files. Therefore the file must contain the type annotations on all but the new types (ISettings implementations).

I've tried to reproduce my problem with the following minimum example:

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using System.Linq;
using Newtonsoft.Json.Serialization;

namespace jsontest
{
    // I can't touch this class, even to add annotations
    // (in practice there are too many classes to update)
    interface IDoNotTouchThis {}
    class DoNotTouchThisClass : IDoNotTouchThis {
        public IMustHaveType MustHaveType => new HasType();
    }

    // This is the old data. It must have type annotations to be deserialized.
    interface IMustHaveType {}
    class HasType : IMustHaveType {}

    // This is the new data. It must not have type annotations.
    // There is a `JsonConverter` to figure out the types according to the fields.
    interface ISettings {}
    class SettingsFoo: ISettings {
        public String Foo => "NotImportant";
    }
    class SettingsBar: ISettings {
        public String Bar => "NotImportant";
        public ISettings SubSettings => new SettingsFoo();
    }

    // This is the top-level class of the data.
    class AllSettings {
        public IDoNotTouchThis MustHaveType => new DoNotTouchThisClass();

        public ISettings MustNotHaveType => new SettingsFoo();

        [JsonProperty(ItemTypeNameHandling = TypeNameHandling.None)] // This helps, but isn't enough
        public IReadOnlyList<ISettings> MustNotHaveTypeEither => new List<ISettings> {
            new SettingsFoo(),
            new SettingsBar(),
        };
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Program.Serialize(new AllSettings()));
        }

        private static JsonSerializerSettings SerializeSettings { get; }
            = new JsonSerializerSettings()
            {
                // For backward compatibility:
                TypeNameHandling = TypeNameHandling.All,
                TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Full,
                ContractResolver = new JsonConverterContractResolver(),
            };

        private static string Serialize<T>(T o)
        {
            return JsonConvert.SerializeObject(
                o,
                Formatting.Indented,
                Program.SerializeSettings);
        }
    }

    public class JsonConverterContractResolver : DefaultContractResolver
    {
        /// <inheritdoc />
        protected override JsonContract CreateContract(Type objectType)
        {
            JsonContract contract = base.CreateContract(objectType);

            // Here I'm hopping to disable type annotations for all `ISettings` instances.
            if (objectType == typeof(ISettings)
                || (objectType.IsClass && objectType.GetInterfaces().Any(i => i == typeof(ISettings)))
                || (objectType == typeof(IReadOnlyList<ISettings>))
                || (objectType == typeof(List<ISettings>)))
            {
                if (contract is JsonContainerContract objectContract)
                {
                    objectContract.ItemTypeNameHandling = TypeNameHandling.None;
                }
            }

            return contract;
        }
    }
}

(.NET Fiddle link)

With this example, we're getting the following:

{
   // Not necessary, but doesn't hurt:
  "$type": "jsontest.AllSettings, pzyc3cmg.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
  "MustHaveType": {
    "$type": "jsontest.DoNotTouchThisClass, pzyc3cmg.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
    "MustHaveType": {
      "$type": "jsontest.HasType, pzyc3cmg.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"
    }
  },
  "MustNotHaveType": {
    "$type": "jsontest.SettingsFoo, pzyc3cmg.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
    "Foo": "NotImportant"
  },
  "MustNotHaveTypeEither": {
    "$type": "System.Collections.Generic.List`1[[jsontest.ISettings, pzyc3cmg.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]], System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
    "$values": [
      {
        "Foo": "NotImportant"
      },
      {
        "Bar": "NotImportant",
        "SubSettings": {
          "Foo": "NotImportant"
        }
      }
    ]
  }
}

instead of

{
   // Not necessary, but doesn't hurt:
  "$type": "jsontest.AllSettings, pzyc3cmg.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
  "MustHaveType": {
    "$type": "jsontest.DoNotTouchThisClass, pzyc3cmg.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
    "MustHaveType": {
      "$type": "jsontest.HasType, pzyc3cmg.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"
    }
  },
  "MustNotHaveType": {
    "Foo": "NotImportant"
  },
  "MustNotHaveTypeEither": [
    {
      "Foo": "NotImportant"
    },
    {
      "Bar": "NotImportant",
      "SubSettings": {
        "Foo": "NotImportant"
      }
    }
  ]
}

I've tried applying TypeNameHandling/ItemTypeNameHandling annotations at different places without success. Is the format I'm looking for possible with Json.NET?

2 Answers

Weirdly, I could only get anything working if I turned it on it's head and disabled type serialization by default, then enabled it on the specific (old) types. That's not ideal in your case because it means you'd need to list all the old types in the resolver which I guess is a lot of work.

private static JsonSerializerSettings SerializeSettings { get; }
    = new JsonSerializerSettings()
    {
        // Disable by default
        TypeNameHandling = TypeNameHandling.None,
        TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Full,
        ContractResolver = new JsonConverterContractResolver(),
    };

public class JsonConverterContractResolver : DefaultContractResolver
{
    /// <inheritdoc />
    protected override JsonContract CreateContract(Type objectType)
    {
        JsonContract contract = base.CreateContract(objectType);

        if (contract is JsonContainerContract objectContract)
        {            
          // enable on old types
          if (typeof(IDoNotTouchThis).IsAssignableFrom(objectType))
          {
            objectContract.ItemTypeNameHandling = TypeNameHandling.All;
          }
        }

        return contract;
    }
}

That produces this output, is that what you need?

{
  "MustNotHaveType": {
    "Foo": "NotImportant"
  },
  "MustHaveType": {
    "MustHaveType": {
      "$type": "jsontest.HasType, a0tq5y51.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"
    }
  },
  "MustNotHaveTypeEither": [
    {
      "Foo": "NotImportant"
    },
    {
      "Bar": "NotImportant",
      "SubSettings": {
        "Foo": "NotImportant"
      }
    }
  ]
}

fiddle:

https://dotnetfiddle.net/ckDb6Z

Update:

I was able to find a way to dynamically set TypeNameHandling on JsonObjectContract properties in CreateContract method, which seems to have solved the issue. Check out .Net fiddler here:

public class JsonConverterContractResolver : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        JsonContract contract = base.CreateContract(objectType);

        if (contract is JsonObjectContract)
        {
            var objectContract = contract as JsonObjectContract;
            foreach (var property in objectContract.Properties)
            {
                var propertyType = property.PropertyType;
                if (IsTypeOfISettings(propertyType))
                {
                    // setting type name handling on property level
                    property.TypeNameHandling = TypeNameHandling.None;
                }
            }
        }

        // Here I'm hopping to disable type annotations for all `ISettings` instances.
        if (IsTypeOfISettings(objectType))
        {
            if (contract is JsonContainerContract objectContract)
            {
                objectContract.ItemTypeNameHandling = TypeNameHandling.None;
            }
        }

        return contract;
    }

    private bool IsTypeOfISettings(Type objectType)
    {
        return objectType == typeof(ISettings)
                || (objectType.IsClass && objectType.GetInterfaces().Any(i => i == typeof(ISettings)))
                || (objectType == typeof(IReadOnlyList<ISettings>))
                || (objectType == typeof(List<ISettings>))
                || (objectType == typeof(Dictionary<string, ISettings>));
    }
}

Original answer:

I believe so, take a look at this updated .NET fiddle. Only change I made in this fiddle was replace ItemTypeNameHandling with TypeNameHandling in AllSettings class properties and it worked! I got inspiration by looking at internal working of Newtonsoft.Json, specifically serializer writer.

class AllSettings {
    public IDoNotTouchThis MustHaveType => new DoNotTouchThisClass();

    [JsonProperty(TypeNameHandling = TypeNameHandling.None)]
    public ISettings MustNotHaveType => new SettingsFoo();

    [JsonProperty(TypeNameHandling = TypeNameHandling.None)]
    public IReadOnlyList<ISettings> MustNotHaveTypeEither => new List<ISettings> {
        new SettingsFoo(),
        new SettingsBar(),
    };
}

Result looks like:

{
  "$type": "jsontest.AllSettings, tciypvk5.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
  "MustHaveType": {
    "$type": "jsontest.DoNotTouchThisClass, tciypvk5.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
    "MustHaveType": {
      "$type": "jsontest.HasType, tciypvk5.exe, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"
    }
  },
  "MustNotHaveType": {
    "Foo": "NotImportant"
  },
  "MustNotHaveTypeEither": [
    {
      "Foo": "NotImportant"
    },
    {
      "Bar": "NotImportant",
      "SubSettings": {
        "Foo": "NotImportant"
      }
    }
  ]
}
Related