-1

I've written a custom JsonConverter that I can assign to JsonSerializerSettings and use with the generic override of JsonConvert.DeserializeObject just fine:

var settings = new JsonSerializerSettings()
{
    TypeNameHandling = TypeNameHandling.All,
    Converters = new List<JsonConverter>() { new MyConverter() }
};
var x = JsonConvert.DeserializeObject<MyType>(input, settings);

The serialized Json was built also using TypeNameHandling.All so it contains type information in a $type field.

In some scenarios, though, I do not know what type was serialized and would like to use the non-generic override of DeserializeObject. I was expecting that, if I use the same settings and/or converter and the Json contained type information that the engine would be able to process the Json correctly.

But it appears that my custom converter is only used for nested objects within the Json and not for the top-most level - despite the $type on each level.

My problem is that without my custom converter I need a default constructor for the class. If I implement that - just for testing - then DeserializeObject indeed returns the correct type. But that isn't a real-life option: amongst other things the custom converter resolves the required instances using an IOC container and then populates them.

Am I missing something or is what I'm asking for simply not possible?

EDIT: Because it was requested, below is some example code. In this example deserializing worked without the default constructor, apparently the other one was called with null values (or default(T)). But the basic problem still exists: the ExampleConverter is not used as I would expect.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;

namespace UnitTests.Serialization
{
    public class Example
    {
        public Example()
        {
            Console.WriteLine("...Example Default Ctor...");
        }

        public Example(Guid guid)
        {
            Console.WriteLine("...Example Ctor: " + guid.ToString());
        }
        public string ExampleProp { get; set; }
    }

    public class ExampleConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer){throw new NotImplementedException();}

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer){
            Console.WriteLine("...ExampleConverter.ReadJson...");
            var result = new Example(Guid.Empty);

            serializer.Populate(reader, result);

            return result;
        }

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(Example);
        }
    }

    [TestClass]
    public class JsonTests
    {
        [TestMethod]
        public void TestDeserializeObject()
        {
            var writeSettings = new JsonSerializerSettings() {TypeNameHandling = TypeNameHandling.All,};
            var readSettings = new JsonSerializerSettings() {TypeNameHandling = TypeNameHandling.All,Converters = new List<JsonConverter>() { new ExampleConverter() }};

            Console.WriteLine("Creating Example...");
            var e1 = new Example(Guid.NewGuid()) { ExampleProp = "some value"};

            Console.WriteLine("\nSerializing e1...");
            var json = Newtonsoft.Json.JsonConvert.SerializeObject(e1, writeSettings);
            Console.WriteLine("e1: " + json);

            Console.WriteLine("\nDeserializing e2 - using DeserializeObject<Example>...");
            var e2 = Newtonsoft.Json.JsonConvert.DeserializeObject<Example>(json, readSettings);
            Console.WriteLine("e2: " +  Newtonsoft.Json.JsonConvert.SerializeObject(e2, writeSettings));

            Console.WriteLine("\nDeserializing e3 - using DeserializeObject...");
            var e3 = Newtonsoft.Json.JsonConvert.DeserializeObject(json, readSettings);
            Console.WriteLine("e3: " + Newtonsoft.Json.JsonConvert.SerializeObject(e2, writeSettings));

        }
    }
}

Output:

Creating Example...
...Example Ctor: d860aa00-4493-4ab0-b681-f0af7b123212

Serializing e1...
e1: {"$type":"UnitTests.Serialization.Example, UnitTests","ExampleProp":"some value"}

Deserializing e2 - using DeserializeObject<Example>...
...ExampleConverter.ReadJson...
...Example Ctor: 00000000-0000-0000-0000-000000000000
e2: {"$type":"UnitTests.Serialization.Example, UnitTests","ExampleProp":"some value"}

Deserializing e3 - using DeserializeObject...
...Example Default Ctor...
e3: {"$type":"UnitTests.Serialization.Example, UnitTests","ExampleProp":"some value"}

EDIT: I also found this, but the answer appears to be wrong: How to deserialize JSON to objects of the correct type, without having to define the type before hand?

dbc
  • 104,963
  • 20
  • 228
  • 340
mike
  • 1,627
  • 1
  • 14
  • 37
  • Could you add minimal classes and example json strings that demonstrate the problem? – Emond May 16 '18 at 15:32
  • Show us the converter, especially the `CanConvert` method. – ProgrammingLlama May 16 '18 at 15:54
  • The basic issue is that `CanConvert` is called with the *declared* type not the *actual type* during deserialization, because the `"$type"` property has not been read (as it will need to be processed inside `ReadJson()`). Take a look at [JSON.NET's Deserializer's Converter selection is not polymorphic (uses declared type, not actual type)](https://stackoverflow.com/q/45639611), [How to deserialize json objects into specific subclasses?](https://stackoverflow.com/q/36584071) and [Using custom JsonConverter and TypeNameHandling in Json.net](https://stackoverflow.com/q/29810004) for workarounds. – dbc May 16 '18 at 18:24
  • wondering: why the down vote? – mike May 17 '18 at 11:31

1 Answers1

0

For my specific use-case the following worked out:

public object DeserializeFromTypedString(string input, JsonSerializerSettings settings)
{
    var r = new Regex(@"^\{\s*""\$type"":\s*""([^""]+)""");

    var m = r.Match(input);
    if (m.Success)
    {
        var t = Type.GetType(m.Groups[1].Value);
        return Newtonsoft.Json.JsonConvert.DeserializeObject(input, t, settings);
    }
    else
    {
        throw new Exception("$type not found!");
    }
}

The idea is to manually parse the string for a $type definition, resolve the type and use that to call the appropriate DeserializeObject overload.

This was good enough for me, because in my specific use-case I was always getting JSON as a string (and not e.g. a Stream or JToken) representing one root object (and not an array). The solution could be adapted accordingly, but I'm still hoping for a better/cleaner solution.

If I add the method and exchange the according lines in my example from above...

Console.WriteLine("\nDeserializing e3 - using DeserializeFromTypeString...");
var e3 = DeserializeFromTypedString(json, readSettings);

...the output is (correctly):

Creating Example...
...Example Ctor: b5af2c3f-1c03-49d8-85c6-f3ff60c9f711

Serializing e1...
e1: {"$type":"UnitTests.Serialization.Example, UnitTests","Guid":"b5af2c3f-1c03-49d8-85c6-f3ff60c9f711","ExampleProp":"some value"}

Deserializing e2 - using DeserializeObject<Example>...
...ExampleConverter.ReadJson...
...Example Ctor: 00000000-0000-0000-0000-000000000000
e2: {"$type":"UnitTests.Serialization.Example, UnitTests","Guid":"00000000-0000-0000-0000-000000000000","ExampleProp":"some value"}

Deserializing e3 - using DeserializeFromTypedString...
...ExampleConverter.ReadJson...
...Example Ctor: 00000000-0000-0000-0000-000000000000
e3: {"$type":"UnitTests.Serialization.Example, UnitTests","Guid":"00000000-0000-0000-0000-000000000000","ExampleProp":"some value"}

Note the output of ExampleConverter.ReadJson and Example Ctor in both deserialization cases which shows that my custom converter is indeed being used. (I also tested it successfully with nested objects in my actual code.)

mike
  • 1,627
  • 1
  • 14
  • 37