Newtonsoft.json serializing and deserializing base/inheirited where classes are from shared projects
Asked Answered
M

1

2

So I have two classes like the ones below. They are both in the same namespace and in the same shared project.

public class Person{
   public string Name{get;set;}
}

public class EmployedPerson : Person{
   public string JobTitle{get;set;}
}

When I serilize these items into rabbitmq I am serializing as the base class like so:

JsonSerializerSettings settings = new JsonSerializerSettings
{
   TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
   TypeNameHandling = TypeNameHandling.Objects
};
JsonConvert.SerializeObject(input, settings)

However when deserializing I run into issues. I would like to be able to do something like shown below where I deserialize as the base class and then check if it is a inheirited type.

Type check:

Person person = Deserialize<Person>(e.Body, Encoding.Unicode);
   if (person is EmployedPerson)
   {
    logger.LogInformation("This person has a job!");
    }

Deserialize settings:

   JsonSerializerSettings settings = new JsonSerializerSettings
   {
      TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
      TypeNameHandling = TypeNameHandling.Auto
   };

Deserialize logic:

    private static T Deserialize<T>(byte[] data, Encoding encoding) where T : class
    {
        try
        {
            using (MemoryStream stream = new MemoryStream(data))
            using (StreamReader reader = new StreamReader(stream, encoding))
                return JsonSerializer.Create(settings).Deserialize(reader, typeof(T)) as T;
        }
        catch (Exception e)
        {
            Type typeParameter = typeof(T);
            logger.LogError(LogEvent.SERIALIZATION_ERROR, e, "Deserializing type {@TypeName} failed", typeParameter.Name);
            logger.LogInformation(Encoding.UTF8.GetString(data));
            return default(T);
        }
    }

Result: The above code fails because the $type property contains the Assembly name and on each end of rabbitmq the assembly name is different because the classes are inside a shared project.

Example error:

Newtonsoft.Json.JsonSerializationException: Error resolving type specified in JSON 'Shared.Objects.EmployedPerson, Person.Dispatcher'. Path '$type', line 1, position 75. ---> System.IO.FileNotFoundException: Could not load file or assembly 'Person.Dispatcher, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.
Meatman answered 28/8, 2017 at 16:7 Comment(7)
I think this might possibly be an answer to your issue: #12382136Summon
Your you could 1) write a custom SerializationBinder. Writing your own binder that sanitizes deserialized types is also a good idea for security reasons as explained here. 2) Emit your own custom type property and parse it with a custom JsonConverter as shown, e.g., in Json.Net Serialization of Type with Polymorphic Child Object.Scape
Looking into SerializationBinder.Meatman
I'm confused. Why is the assembly name different on each end of the queue if the types are in a shared project?Mediterranean
because shared projects don't create their own assemblies. They are assimilated into the project that is using it.Meatman
For more on shared projects visit here: #30635253Meatman
Hey anyone know how to handle Lists of complex types? They all Come through as "List`1"....Meatman
M
5

Thank you @dbc, your suggestion to write a custom SerializationBinder is, as far as I can tell, the best solution to my problem.

I used the KnownTypesBinder as implemented at: https://www.newtonsoft.com/json/help/html/SerializeSerializationBinder.htm

KnownTypesBinder:

public class KnownTypesBinder : ISerializationBinder
    {
        public IList<Type> KnownTypes { get; set; }

        public Type BindToType(string assemblyName, string typeName)
        {
            return KnownTypes.SingleOrDefault(t => t.Name == typeName);
        }

        public void BindToName(Type serializedType, out string assemblyName, out string typeName)
        {
            assemblyName = null;
            typeName = serializedType.Name;
        }
    }

JsonSerializerSettings with the SerializationBinder set to an instance of KnownTypesBinder was used on both the serializing and deserializing endpoints. I probably only need it for the deserializing end, but put it in both for consistency.

settings = new JsonSerializerSettings
{
    TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
    TypeNameHandling = TypeNameHandling.Objects,
    SerializationBinder = new KnownTypesBinder()
};

After creating a settings object then I pass it into the JsonConvert serialization functions.

JsonConvert.DeserializeObject<T>(Encoding.Unicode.GetString(input), settings) 

Also note that KnownTypes in KnownTypesBinder must be prepopulated with all of the non primitive types you will be deserializing.

Edit: I am currently not accepting my own answer because I have no idea how to handle List of complex types. For instance if a Person has a List and a List, what type do you return when the typeName is "List`1" and it could be either one.

Edit The following version of the KnownTypesBinder solved my issues related to Lists of objects.

public class KnownTypesBinder: ISerializationBinder
{
    public IList<Type> KnownTypes { get; set; }

    public Type BindToType(string assemblyName, string typeName)
    {
        return KnownTypes.SingleOrDefault(t => t.UnderlyingSystemType.ToString() == typeName);
    }

    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = null;
        typeName = serializedType.UnderlyingSystemType.ToString();
    }
}
Meatman answered 29/8, 2017 at 15:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.