0

I would like to be able to ask JSON.net to deserialize a JSON string into an interface, and return one of the classes that implements that interface. Is that possible?

We tried to create our own JsonConverter, but got an exception with message "Could not create an instance of type {0}. Type is an interface or abstract class and cannot be instantiated." (from Newtonsoft.Json.Serialization.JsonSerializerInternalReader). It looks like JSON.net explicitly checks the type before it makes it to ReadJson() override in our custom converter.

EDIT: added mcve

public interface IMessage { }
public class ErrorMessage : IMessage { public string message; }
public class WarnMessage : IMessage { public List<string> warnings; }

public class Messages {
    public string id;
    public List<IMessage> messages;
}

public class MessageJsonConverter : JsonConverter
{
    private readonly ISet<Type> _types;

    public MessageJsonConverter()
    {
        IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany( s => s.GetTypes() )
            .Where( p => typeof( IMessage ).IsAssignableFrom( p ) )
            .Where( p => p.IsClass );

        _types = new HashSet<Type>( types );
    }

    public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
    {
        // Never hits breakpoint here
        if( !CanConvert( value.GetType() ) )
        {
            throw new ArgumentException( "Invalid type." );
        }

        JToken t = JToken.FromObject( value );
        t.WriteTo( writer );
    }

    public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer )
    {
        throw new NotImplementedException( "Can be done without a custom JsonConverter." );
    }

    public override bool CanConvert( Type objectType )
    {
        return _types.Any( t => t == objectType );
    }
}

...

public class Program
{
    public static void Main( string[] args )
    {
        IMessage errorMessage = new ErrorMessage() { message = "error" };

        string errorMessageJson = JsonConvert.SerializeObject( errorMessage );
        Console.Out.WriteLine( errorMessageJson );
        // {"message":"error"}

        // throws Newtonsoft.Json.JsonSerializationException
        IMessage deserializedErrorMesage = JsonConvert.DeserializeObject<IMessage>(
            errorMessageJson,
            new MessageJsonConverter()
        );

        #region Interesting case that also fails
        IMessage warnMessage = new WarnMessage() { warnings = new List<string>() { "warn 1", "warn 2" } };
        Messages messages = new Messages() { id = "batch1", messages = new List<IMessage> { errorMessage, warnMessage } };
        string messagesJson = JsonConvert.SerializeObject( messages );

        // throws Newtonsoft.Json.JsonSerializationException
        IMessage deserializedMessages = JsonConvert.DeserializeObject<IMessage>(
            messagesJson,
            new MessageJsonConverter()
        );
        #endregion

        Console.In.Read();
    }
}
neverendingqs
  • 4,006
  • 3
  • 29
  • 57

1 Answers1

1

According to Json.net release notes after the relase of 6.0 -

"Json.NET no longer immediately throws an exception if it tries to deserialize an interface or abstract type. If you have specified a way for that type to be created, such as resolving it from a dependency inject framework, then Json.NET will happily continue deserializing using that instance."

So you can use any custom or third party DI container to supply that typed instance based on provided interface/base class. You just need to provide your resolver here.

public class AutofacContractResolver : DefaultContractResolver
{
    private readonly IContainer _container;

    public AutofacContractResolver(IContainer container)
    {
        _container = container;
    }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        JsonObjectContract contract = base.CreateObjectContract(objectType);

        // use Autofac to create types that have been registered with it
        if (_container.IsRegistered(objectType))
            contract.DefaultCreator = () => _container.Resolve(objectType);

        return contract;
    }
}

See full details at Source.

vendettamit
  • 14,315
  • 2
  • 32
  • 54