Newtonsoft.Json icon indicating copy to clipboard operation
Newtonsoft.Json copied to clipboard

Serialization should respect TypeForwardedFrom attribute

Open maciejjarzynski opened this issue 4 years ago • 3 comments

Hello, I'm coming from a discussion in https://github.com/dotnet/runtime/issues/47113 and basically basing on the response https://github.com/dotnet/runtime/issues/47113#issuecomment-762256639 I'd like to propose/ask for handling that TypeForwardedFrom attribute in a DefaultSerializationBinder. In a current solution, backward compatibility for a case described below is broken, because $type for HashSet<> serialized in net5.0 could not be deserialized in previous versions of dotnet, i.e. netcoreapp3.1. By using TypeForwardedFromAttribute it would allow to be independent of types being moved back and forth between assemblies, what happens quite often according to https://github.com/dotnet/runtime/issues/47113#issuecomment-762586943 and also with an assumption that TypeForwardedFromAttribute is the way to ensure backward compatibility. I reckon the solution for that would be quite simple, as in the custom serialization binder:

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

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

But I think it would be best to have that logic implemented by default in DefaultSerializationBinder. @JamesNK are you okay with incorporating that logic in DefaultSerializationBinder and would accept such a contribution? Or if not, is there anything against that, that I may not be aware of?

Source type

    public class DTO
    {
        public DTO(ICollection<string> collection)
        {
            Collection = collection;
        }

        public ICollection<string> Collection { get; }
    }

Actual behavior

(when it's being serialized with HashSet<> given as collection) - net5.0

{
  "Collection": {
    "$type": "System.Collections.Generic.HashSet`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib",
    "$values": [
      "value"
    ]
  }
}

Expected behavior

The expected behavior would be to respect TypeForwarderFrom attribute on a type during serialization to allow backward and forward compatibility, so the result would be

{
  "Collection": {
    "$type": "System.Collections.Generic.HashSet`1[[System.String, System.Private.CoreLib]], System.Core",
    "$values": [
      "value"
    ]
  }
}

where the assembly name is being taken from TypeForwarderFromAttribute on HashSet class.

maciejjarzynski avatar Jan 19 '21 11:01 maciejjarzynski

This seems to work for all cases (netcore to net48, net48 to netcore and netcore to netcore). Only tested for TypeNameAssemblyFormatHandling.Simple.

/// <summary>
/// Core lib assembly name.
/// </summary>
private static readonly string CoreLibAssembly = typeof(object).Assembly.FullName;

/// <summary>
/// Mscore lib assembly name.
/// </summary>
private static readonly string MscorlibAssembly = ((TypeForwardedFromAttribute)Attribute.GetCustomAttribute(typeof(object), typeof(TypeForwardedFromAttribute), false)).AssemblyFullName;

/// <summary>
/// Controls the binding of a serialized object to a type.
/// </summary>
/// <param name="serializedType"></param>
/// <param name="assemblyName"></param>
/// <param name="typeName"></param>
public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
    base.BindToName(serializedType, out assemblyName, out typeName);

    if (!serializedType.IsValueType && Attribute.GetCustomAttribute(serializedType, typeof(TypeForwardedFromAttribute), false) is TypeForwardedFromAttribute attribute)
    {
        assemblyName = attribute.AssemblyFullName;
    }
    //Value types or enumerable that don't have type forwarded.
    else if (assemblyName == CoreLibAssembly)
    {
        assemblyName = MscorlibAssembly;
    }

    if (typeName.Contains(CoreLibAssembly))
    {
        typeName = typeName.Replace(CoreLibAssembly, MscorlibAssembly);
    }
}

malylemire1 avatar Dec 17 '21 16:12 malylemire1

I guess, this is the reason I cannot deserialize a HashSet<Guid> in a .NET Framework 4.8 application, which has been serialized in a .NET (Core) 8 application, although serialization and deserialization have been implemented in a common .NET Standard 2.0 project.

Additional information:

  • Our use-case is required to use Newtonsoft.Json.TypeNameHandling.Auto.

Given the longevity of this issue, I guess, there's no intention of actually fixing this, right?

Matthias-Heinz avatar Oct 31 '24 10:10 Matthias-Heinz

Right. As all other issues in that repo.

sungam3r avatar Nov 15 '24 21:11 sungam3r