1

I have a class that I use as a message type in Rebus to publish/subscribe to, but I hit a snag when I was trying out a proof-of-concept in LinqPad. Any messages that my app receives fails with a deserialization exception. I have been able to narrow down the issue to Newtonsoft.JSON package and come up with the minimal example to demonstrate the issue:

public class MyMessage
{
    public string Name { get; set; } = "";
    public int Number { get; set; }
}

void Main()
{
    var message = new MyMessage { Name = "ABC", Number = 5 };
    
    var defaultSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };
    
    var serialized = JsonConvert.SerializeObject(message, defaultSettings);
    
    Console.WriteLine(serialized);

    try
    {
        var deserialized = JsonConvert.DeserializeObject(serialized, message.GetType(), defaultSettings);
        Console.WriteLine(deserialized);
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }
}

This code works fine as a console app if I compile it in Rider, and outputs the following to the console:

{"$type":"ConsoleApp1.MyMessage, ConsoleApp1","Name":"ABC","Number":5}
ConsoleApp1.MyMessage

The same code fails in LinqPad 6 on the line having JsonConvert.DeserializeObject statement with the following error message:

{"$type":"UserQuery+MyMessage, LINQPadQuery","Name":"ABC","Number":5}
Newtonsoft.Json.JsonSerializationException: Type specified in JSON 'UserQuery+MyMessage, LINQPadQuery, Version=1.0.0.5, Culture=neutral, PublicKeyToken=null' is not compatible with 'UserQuery+MyMessage, LINQPadQuery, Version=1.0.0.61, Culture=neutral, PublicKeyToken=null'. Path '$type', line 1, position 44.
   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)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at UserQuery.Main() in C:\Users\arcaa\AppData\Local\Temp\LINQPad6\_dwfnlfnz\tufqxf\LINQPadQuery:line 19

Exception message: Type specified in JSON 'UserQuery+MyMessage, LINQPadQuery, Version=1.0.0.5, Culture=neutral, PublicKeyToken=null' is not compatible with 'UserQuery+MyMessage, LINQPadQuery, Version=1.0.0.61, Culture=neutral, PublicKeyToken=null

It looks as if the serializer is attempting to deserialize the string into a different version of the type (UserQuery+MyMessage, LINQPadQuery, Version=1.0.0.5) than the one provided as a parameter (UserQuery+MyMessage, LINQPadQuery, Version=1.0.0.61). The interesting bit here is the type used in the JsonConvert.DeserializeObject method is the exact same type that's passed to the serializer and there's only one class in that code.

I don't know where the other type version comes from. Does anyone know the reason for this and if possible, how to avoid it?

Edit:

I also noticed that if I close LinqPad and open it again, the code works fine. But if I then go and make a change to the code (even if I just add a single space), the exception returns.

Arca Artem
  • 1,063
  • 1
  • 10
  • 20
  • Newtonsoft's [`DefaultSerializationBinder`](https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/DefaultSerializationBinder.cs) has some logic that uses `Assembly.LoadWithPartialName(assemblyName);` to *`check the GAC for a partial name`*. Maybe that's your problem? Try debugging `DefaultSerializationBinder` on Linqpad to see what happens. If it isn't working you may need to write a custom serialization binder such as the one from [Custom $type value for serialized objects](https://stackoverflow.com/q/49283251/3744182). – dbc Jul 26 '21 at 22:59
  • 1
    This is a good idea for security reasons, see [TypeNameHandling caution in Newtonsoft Json](https://stackoverflow.com/q/39565954/3744182) for why. – dbc Jul 26 '21 at 22:59

1 Answers1

2

Most likely, there will be some static dictionary in the Newtonsoft.Json library that caches type information, keyed to strings such as "MyMessage". When you edit the query re-run it, LINQPad must re-compile the query, so there will be a new version of MyMessage sitting in a new DLL. However, the static cache in Newtonsoft.Json will still point to the old one was used in the last execution.

To work around this, you don't need to re-start LINQPad; just press Shift+F5 to clear the cached process. Alternatively, add the following code to your query:

Util.NewProcess = true;

This tells LINQPad not to cache the process for subsequent use.

There's also an option in Edit | Preferences > Advanced to never recycle processes. This incurs a small performance cost; also methods such as LINQPad's Util.Cache - which caches data between executions - will not work.

Joe Albahari
  • 30,118
  • 7
  • 80
  • 91