Just a small note for myself: serializing objects in one framework and deserialize it in another can become a real PITA.

For example, I serialize an object in .NET Core app, send it over to another system which uses .NET Framework 4.x and then I realize that objects can’t be deserialized anymore. I’m using Newtonsoft.Json on both ends, so what’s the issue?

Problem

Turns out that when you use JsonSerializerSettings.TypeNameHandling = TypeNameHandling.All, your serialized objects look like this:

{
  "$type": "PF.Tests.MyModel, PF.Tests",
  "dict": {
    "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib",
    "something": -2147483648
  },
  "firstName": "test",
  "id": 123
}

Serializer adds assemblies next to types you use. System.Private.CoreLib does not exist in .NET Framework and the error is thrown.

Fix

In order to fix the problem you can use your own SerializationBinder. That’s what actually Newtonsoft.JSON’s author recommends.

Option 1

Some .NET Core classes use the special attribute that tells which class in .NET Framework it corresponds, so we can use that.

public class NetCoreSerializationBinder : DefaultSerializationBinder
{
    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        base.BindToName(serializedType, out assemblyName, out typeName);

        if (Attribute.GetCustomAttribute(serializedType, typeof(TypeForwardedFromAttribute), false) is TypeForwardedFromAttribute attr)
            assemblyName = attr.AssemblyFullName;
    }
}

Taken from https://stackoverflow.com/a/59992171/1160233.

Option 2

This is what worked for me.

public class NetCoreSerializationBinder : DefaultSerializationBinder
{
    private static readonly Regex regex = new Regex(
        @"System\.Private\.CoreLib(, Version=[\d\.]+)?(, Culture=[\w-]+)(, PublicKeyToken=[\w\d]+)?");

    private static readonly ConcurrentDictionary<Type, (string assembly, string type)> cache =
        new ConcurrentDictionary<Type, (string, string)>();

    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        base.BindToName(serializedType, out assemblyName, out typeName);

        if (cache.TryGetValue(serializedType, out var name))
        {
            assemblyName = name.assembly;
            typeName = name.type;
        }
        else
        {
            if (assemblyName.AsSpan().Contains("System.Private.CoreLib".AsSpan(), StringComparison.OrdinalIgnoreCase))
                assemblyName = regex.Replace(assemblyName, "mscorlib");

            if (typeName.AsSpan().Contains("System.Private.CoreLib".AsSpan(), StringComparison.OrdinalIgnoreCase))
                typeName = regex.Replace(typeName, "mscorlib");

            cache.TryAdd(serializedType, (assemblyName, typeName));
        }
    }
}

It’s not as elegant as the first solution but it does the trick and replaces System.Private.CoreLib in all occurencies.

Found this solution here: https://stackoverflow.com/a/56184385/1160233.