如何忽略Json.NET反序列化中的特定字典键?

时间:2018-08-17 19:05:50

标签: c# json.net

如何反序列化以下JSON

{  
  "result"     : {
    "master"   : [
      ["one", "two"],
      ["three", "four"],
      ["five", "six", "seven"],
    ],
    "blaster"  : [
      ["ein", "zwei"],
      ["drei", "vier"]
    ],
    "surprise" : "nonsense-nonsense-nonsense"
  }
}

进入以下数据结构

class ResultView
{
  public Dictionary<string, string[][]> Result { get; set; }
}

使用Json.NET吗?

它必须是字典,因为在编译时,诸如“ master”和“ blaster”之类的键名是未知的。众所周知,它们始终指向字符串数组的数组。问题在于,名称已知且始终相同的关键“惊奇”指向无法解释为string[][]的事物,这导致Json.NET中出现异常。

有什么方法可以使Json.NET忽略特定的字典键吗?

2 个答案:

答案 0 :(得分:0)

我认为您可以忽略这样的异常:

ResultView result = JsonConvert.DeserializeObject<ResultView>(jsonString,
       new JsonSerializerSettings
       {
            Error = delegate (object sender, Newtonsoft.Json.Serialization.ErrorEventArgs args)
            {
                // System.Diagnostics.Debug.WriteLine(args.ErrorContext.Error.Message);
                args.ErrorContext.Handled = true;
            }
        }
    );

args.ErrorContext.Error.Message将包含实际的错误消息。

args.ErrorContext.Handled = true;将告诉Json.Net继续。

答案 1 :(得分:0)

您可以为IDictionary<string, TValue>引入custom generic JsonConverter,以过滤掉无效的字典值(即那些不能成功反序列化为字典值类型的值):

public class TolerantDictionaryItemConverter<TDictionary, TValue> : JsonConverter where TDictionary : IDictionary<string, TValue>
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(TDictionary).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type dictionaryType, object existingValue, JsonSerializer serializer)
    {
        // Get contract information
        var contract = serializer.ContractResolver.ResolveContract(dictionaryType) as JsonDictionaryContract;
        if (contract == null)
            throw new JsonSerializationException(string.Format("Invalid JsonDictionaryContract for {0}", dictionaryType));
        if (contract.DictionaryKeyType != typeof(string))
            throw new JsonSerializationException(string.Format("Key type {0} not supported", dictionaryType));
        var itemContract = serializer.ContractResolver.ResolveContract(contract.DictionaryValueType);

        // Process the first token
        var tokenType = reader.SkipComments().TokenType;
        if (tokenType == JsonToken.Null)
            return null;
        if (reader.TokenType != JsonToken.StartObject)
            throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path));

        // Allocate the dictionary
        var dictionary = existingValue as IDictionary<string, TValue> ?? (IDictionary<string, TValue>) contract.DefaultCreator();

        // Process the collection items
        while (reader.Read())
        {
            if (reader.TokenType == JsonToken.EndObject)
            {
                return dictionary;
            }
            else if (reader.TokenType == JsonToken.PropertyName)
            {
                var key = (string)reader.Value;
                reader.ReadSkipCommentsAndAssert();

                // For performance, skip tokens we can easily determine cannot be deserialized to itemContract
                if (itemContract.QuickRejectStartToken(reader.TokenType))
                {
                    System.Diagnostics.Debug.WriteLine(string.Format("value for {0} skipped", key));
                    reader.Skip();
                }
                else
                {
                    // What we want to do is to distinguish between JSON files that are not WELL-FORMED
                    // (e.g. truncated) and that are not VALID (cannot be deserialized to the current item type).
                    // An exception must still be thrown for an ill-formed file.
                    // Thus we first load into a JToken, then deserialize.
                    var token = JToken.Load(reader);
                    try
                    {
                        var value = serializer.Deserialize<TValue>(token.CreateReader());
                        dictionary.Add(key, value);
                    }
                    catch (Exception)
                    {
                        System.Diagnostics.Debug.WriteLine(string.Format("value for {0} skipped", key));
                    }
                }
            }
            else if (reader.TokenType == JsonToken.Comment)
            {
                continue;
            }
            else
            {
                throw new JsonSerializationException(string.Format("Unexpected token type {0} object at path {1}.", reader.TokenType, reader.Path));
            }
        }
        // Should not come here.
        throw new JsonSerializationException("Unclosed object at path: " + reader.Path);
    }

    public override bool CanWrite { get { return false; } }

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

public static partial class JsonExtensions
{
    public static JsonReader SkipComments(this JsonReader reader)
    {
        while (reader.TokenType == JsonToken.Comment && reader.Read())
            ;
        return reader;
    }

    public static void ReadSkipCommentsAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        while (reader.Read())
        {
            if (reader.TokenType != JsonToken.Comment)
                return;
        }
        new JsonReaderException(string.Format("Unexpected end at path {0}", reader.Path));
    }

    internal static bool QuickRejectStartToken(this JsonContract contract, JsonToken token)
    {
        if (contract is JsonLinqContract)
            return false;
        switch (token)
        {
            case JsonToken.None:
                return true;

            case JsonToken.StartObject:
                return !(contract is JsonContainerContract) || contract is JsonArrayContract; // reject if not dictionary or object

            case JsonToken.StartArray:
                return !(contract is JsonArrayContract); // reject if not array

            case JsonToken.Null:
                return contract.CreatedType.IsValueType && Nullable.GetUnderlyingType(contract.UnderlyingType) == null;

            // Primitives
            case JsonToken.Integer:
            case JsonToken.Float:
            case JsonToken.String:
            case JsonToken.Boolean:
            case JsonToken.Undefined:
            case JsonToken.Date:
            case JsonToken.Bytes:
                return !(contract is JsonPrimitiveContract); // reject if not primitive.

            default:
                return false;
        }
    }
}

然后您可以将其添加到设置中,如下所示:

var settings = new JsonSerializerSettings
{
    Converters = { new TolerantDictionaryItemConverter<IDictionary<string, TValue>, TValue>() },
};

var root = JsonConvert.DeserializeObject<ResultView>(json, settings);

或使用JsonConverterAttribute将其直接添加到ResultView

class ResultView
{
    [JsonConverter(typeof(TolerantDictionaryItemConverter<IDictionary<string, string[][]>, string[][]>))]
    public Dictionary<string, string[][]> Result { get; set; }
}

注意:

  • 我以通用的方式编写了转换器,以处理任何类型的字典值,包括诸如intDateTime之类的基元以及数组或对象。

  • 虽然具有无效字典值的JSON文件(无法反序列化为字典值类型的JSON文件)应可反序列化,但是格式错误 JSON文件(例如,被截断的JSON文件)应该仍然会引发异常。

    转换器首先通过将值加载到JToken中,然后尝试反序列化令牌来处理此问题。如果文件格式不正确,JToken.Load(reader)将引发异常,该异常有意未被捕获。

  • 据报道,Json.NET的异常处理“非常不稳定”(例如,参见Issue #1580: Regression from Json.NET v6: cannot skip an invalid object value type in an array via exception handling),因此我没有依靠它来跳过无效的字典值。

  • 我不确定100%是否正确处理了所有评论。因此可能需要其他测试。

工作示例.Net小提琴here