当某些 $type 未知时使用 TypeNameHandling 反序列化一组对象的最佳方法

Best way to deserialize set of objects with TypeNameHandling when some $type are unknown

我目前正在使用 json.net 反序列化和实例化一组来自 REST 服务的对象。

此服务为我提供了许多对象类型,因此我必须使用 TypeNameHandling.Objects 和自定义活页夹来匹配我的程序集类型。

来自服务的响应示例:

{
    "rows":[
        {
            "id":"id1",
            "doc":{
                "$type":"Car",
                "color":"blue"
            }
        },
        {
            "id":"id36",
            "doc":{
                "$type":"Dog",
                "name":"hodor"
            }
        },
        {
            "id":"id52",
            "doc":{
                "$type":"Human",
                "name":"gandalf"
            }
        }
    ]
}

我的程序集中的类型是:

行数:

public class Rows
{
    public List<Row> rows {get; set;}
}

行:

public class Row
{
    public string id {get; set;}
    public IDocument doc {get; set;}
}   

IDocument:

public class Dog : IDocument
{
    public string id {get; set;}
    public string name {get; set;}
}

汽车:

public class Car : IDocument
{
    public string id {get; set;}
    public string color {get; set;}
}

狗:

public class Dog : IDocument
{
    public string id {get; set;}
    public string name {get; set;}
}

我想使用自定义活页夹将此答案反序列化为 Rows 对象:

public class DocumentBinder : ISerializationBinder
{
    public Type BindToType(string assemblyName, string typeName)
    {
        if (typeName == "Dog")
            return typeof(Dog);
        else if (typeName == "Car")
            return typeof(Car);
        throw new JsonSerializationException();
    }

    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = null;
        if (serializedType == typeof(Dog))
            typeName = "Dog";
        else if (serializedType == typeof(Car))
            typeName = "Car";
        else
            throw new JsonSerializationException();
    }
}

我的问题是其中一些类型在我的程序集中是未知的,假设在我的示例中是 Human,我不想建模 Human。我可以这样做,但可以更新 REST 服务以添加新类型,而我对该服务没有任何控制权。

我发现从该服务反序列化所有对象的唯一方法是:

  1. 使用 JObject.Parse()
  2. 将 Rows 对象转换为 JObject
  3. 遍历 rows 以逐一反序列化对象,忽略 $type 未知时抛出的 JsonSerializationException

它可以工作,但性能很差,它需要在开始反序列化之前转储整个 HTTP 应答。

为了提高性能我想使用 StreamReaderJsonTextReader 但我不能,只要我找不到方法告诉 json.net 忽略未知 $type 和 return 一个空值(如果需要)。 如果所有类型都是已知的,它会按预期与流一起工作,但一旦缺少一种类型,它就会抛出 JsonSerializationException.

在我的示例中,对于完全反序列化的 Row 对象中的 Human 类型,我希望 doc 为 null。

有没有办法将未知类型视为 null?我需要自己的转换器吗?

你可以适应 from and throw a custom exception, inheriting from JsonSerializationException, from your custom serialization binder. Then, handle that exception using Newtonsoft's exception handling mechanism.

按如下方式修改您的活页夹:

public class DocumentBinder : KnownTypesBinder
{
    static readonly Dictionary<string, Type> nameToType = new Dictionary<string, Type>
    {
        {"Dog", typeof(Dog)},
        {"Car", typeof(Car)},
    };
    public DocumentBinder() : base(nameToType) { }
}

public class KnownTypesBinder : ISerializationBinder
{
    readonly Dictionary<string, Type> nameToType;
    readonly Dictionary<Type, string> typeToName;

    public KnownTypesBinder(IEnumerable<KeyValuePair<string, Type>> nameToType)
    {
        this.nameToType = nameToType.ToDictionary(p => p.Key, p => p.Value);
        this.typeToName = nameToType.ToDictionary(p => p.Value, p => p.Key);
    }

    public Type BindToType(string assemblyName, string typeName)
    {
        if(nameToType.TryGetValue(typeName, out var type))
            return type;
        throw new JsonSerializationBinderException(string.Format("Unknown type name {0} ({1})", typeName, assemblyName));
    }

    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        if(!typeToName.TryGetValue(serializedType, out typeName))
            throw new JsonSerializationBinderException(string.Format("Unknown type {0}", serializedType));
        assemblyName = null;
    }
}

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) { }
}

然后按如下方式初始化您的 JsonSerializerSettings

var deserializationSettings = new JsonSerializerSettings
{
    SerializationBinder = new DocumentBinder(),
    TypeNameHandling = TypeNameHandling.Objects, // Or Auto as appropriate
    Error = (sender, args) =>
    {
        if (args.CurrentObject == args.ErrorContext.OriginalObject
            && args.ErrorContext.Error.GetBaseException() is JsonSerializationBinderException
            )
        {
            args.ErrorContext.Handled = true;
        }
    },
    // Other settings as required
};

使用这些设置,您将能够直接从流中反序列化 JSON 并跳过未知的 IDocument 类型,而无需加载和预处理 JToken 层次结构。

备注:

  • Json.NET可能会将一个应用程序抛出的异常包裹在多层异常中,所以必须使用GetBaseException()来判断是否抛出自定义异常。

  • 您可能不想在 序列化 时处理 JsonSerializationBinderException,因为序列化时出现异常将表明您的活页夹中存在错误或缺少类型。

  • 通过只处理您的自定义异常,您可以确保不会吞噬其他错误引起的异常。例如。由格式错误的 JSON 文件引起的 JsonReaderException 永远不应被吞下,因为这样做可能会导致 JsonTextReader 陷入无限循环。

演示 fiddle here.