Search code examples
c#json.net

"Type specified in JSON is not compatible" in Newtonsoft for classes appearing in different loaded Assemblies


We have a somewhat weird setup where we have a C# SDK which can host different assemblies that are loaded at runtime. We are using the McMaster DotNetCorePlugins library to load these assemblies. (The SDK is actually loaded into a Java service that uses JNI to communicate with it, but I don't think that's material to the issue.)

We want to be able to host different versions of an assembly that are loaded side-by-side in the same service/SDK instance, to support parallel development. So the assemblies are mostly identical in terms of classes, namespaces, etc. (They are typically on the same assembly version too, but I've tested using different assembly versions and it does not appear to make a difference)

However, when we load a second assembly and it wants to deserialize some JSON, we get an exception:

Newtonsoft.Json.JsonSerializationException: Type specified in JSON 'LoadedAssembly.BaseClass, LoadedAssembly, Version=4.16.13.0, Culture=neutral, PublicKeyToken=null' is not compatible with 'LoadedAssembly.DerivedClass, LoadedAssembly, Version=4.16.14.0, Culture=neutral, PublicKeyToken=null'. Path 'Base[0].ruleTarget.groupingSize.$type', line 13, position 68. at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolveTypeName(JsonReader reader, Type& objectType, JsonContract& contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, String qualifiedTypeName) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadMetadataProperties(JsonReader reader, Type& objectType, JsonContract& contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue, Object& newValue, String& id)

...

It seems like the type information from the first loaded assembly is somehow getting mixed up with the type information from the second loaded assembly in a way that causes this error. Is there any way to work around this issue?

(Newtonsoft 12.0.3, .NET 5.0)


Solution

  • Thanks to user stuartd for pointing me in the direction of TypeNameHandling. As it turns out, setting TypeNameAssemblyFormatHandling.Full in JsonSerializerSettings to use fully qualified assembly names in resolution and caching of type information will keep the assemblies' type information separated, provided that the loaded assemblies have different assembly versions.

    That's the easy way. If it's not practical to require that the assemblies have different versions (which is true in my case), there is an alternative. The cache which causes the type information to become intermingled is implemented in the Newtonsoft class DefaultSerializationBinder. You can replace this with a custom SerializationBinder that keeps the cache entries for the "identical" assemblies separated.

    I was able to do this by copying the DefaultSerializationBinder code (plus a few supporting utility classes and methods) into my project, in order to change private and internal stuff. I'm not going to post the full change here, but the heart of it was this:

    private string cacheVersion { get; } = $"_{DateTime.UtcNow.Millisecond}";
    private StructMultiKey<string, string> DecoratedCacheKey(string assemblyName, string typeName)
    {
        string decoratedAssemblyName = assemblyName != null ? $"{assemblyName}{cacheVersion}" : assemblyName;
        return new StructMultiKey<string, string>(decoratedAssemblyName, typeName);
    }
    

    Since the assemblies are loaded at different times, different cacheVersions are created for each. Then, using DecoratedCacheKey to store the cached types keeps them separated. Even for assemblies with the same name and version!

    (Of course types that could be shared are also kept separate; you could write more code to only separate the entries belonging to particular assemblies if you cared to do so.)