Search code examples
c#jsonserializationjson.net

Ignore missing types during deserialization of list


When deserializing a list with TypeNameHandling.All, if a type namespace for one of the items is missing (deleted after serialization), it will cause a Error resolving type specified in JSON error.
I wish to ignore these items instead, leaving the rest behind.

Error = (sender, args) => { args.ErrorContext.Handled = true; } in JsonSerializerSettings does what I'm looking for, but will capture ALL errors of course.

Is there a cleaner way of doing this, maybe via a serializer setting I've missed?


Solution

  • You can use the following properties of ErrorContext inside a SerializationErrorCallback to restrict the types of errors to be handled and ignored:

    • ErrorEventArgs.ErrorContext.OriginalObject: this gets the original object that caused the error. When list item cannot be constructucted due to an invalid type name, the OriginalObject will be the list itself.

      With this property, you could check to see whether OriginalObject is an IList<T> for some T.

    • ErrorEventArgs.CurrentObject. Gets the current object the error event is being raised against. Exceptions bubble up the serialization call stack and objects at each level can attempt to handle the error.

      You will want to handle it at the lowest level, when CurrentObject == ErrorContext.OriginalObject.

    • ErrorEventArgs.ErrorContext.Error - gets the actual exception thrown. You will want to handle only exceptions thrown by the serialization binder.

    Now, how to detect and trap only those exceptions due to failed type name binding? As it turns out, Json.NET's DefaultSerializationBinder throws a JsonSerializationException when a type cannot be loaded. However, the same exception can be thrown in many other situations, including a malformed JSON file. So, introduce an ISerializationBinder decorator that catches and traps exceptions from the default JSON binder and packages them into a specific exception type:

    public class JsonSerializationBinder : ISerializationBinder
    {
        readonly ISerializationBinder binder;
    
        public JsonSerializationBinder(ISerializationBinder binder)
        {
            if (binder == null)
                throw new ArgumentNullException();
            this.binder = binder;
        }
    
        public Type BindToType(string assemblyName, string typeName)
        {
            try
            {
                return binder.BindToType(assemblyName, typeName);
            }
            catch (Exception ex)
            {
                throw new JsonSerializationBinderException(ex.Message, ex);
            }
        }
    
        public void BindToName(Type serializedType, out string assemblyName, out string typeName)
        {
            binder.BindToName(serializedType, out assemblyName, out typeName);
        }
    }
    
    public class JsonSerializationBinderException : JsonSerializationException
    {
        public JsonSerializationBinderException() { }
    
        public JsonSerializationBinderException(string message) : base(message) { }
    
        public JsonSerializationBinderException(string message, Exception innerException) : base(message, innerException) { }
    
        public JsonSerializationBinderException(SerializationInfo info, StreamingContext context) : base(info, context) { }
    }
    

    Further, at some higher level in the code Json.NET packages the JsonSerializationBinderException inside yet another JsonSerializationException, so it's necessary to look through the inner exceptions for an exception of the necessary type when deciding whether to handle an exception. The following settings does the job:

    var settings = new JsonSerializerSettings
    {
        SerializationBinder = new JsonSerializationBinder(new DefaultSerializationBinder()),
        TypeNameHandling = TypeNameHandling.All, // Or Auto or Objects as appropriate
        Error = (sender, args) =>
        {
            if (args.CurrentObject == args.ErrorContext.OriginalObject
                && args.ErrorContext.Error.InnerExceptionsAndSelf().OfType<JsonSerializationBinderException>().Any()
                && args.ErrorContext.OriginalObject.GetType().GetInterfaces().Any(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IList<>)))
            {
                args.ErrorContext.Handled = true;
            }
        },
    };
    

    Using the extension method:

    public static class ExceptionExtensions
    {
        public static IEnumerable<Exception> InnerExceptionsAndSelf(this Exception ex)
        {
            while (ex != null)
            {
                yield return ex;
                ex = ex.InnerException;
            }
        }
    }
    

    Demo fiddle #1 here.

    Note that ISerializationBinder was introduced in Json.NET 10.0.1. In earlier versions your wrapper must inherit from SerializationBinder and be set in JsonSerializerSettings.Binder.

    Demo fiddle #2 here.