简体   繁体   中英

How to ignore specific dictionary key in Json.NET deserialization?

How can one deserialize the following JSON

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

into the following data structure

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

with Json.NET?

It has to be dictionary because key names such as 'master' and 'blaster' are unknown at the time of compilation. What is known is that they always point to an array of arrays of strings. The problem is that key 'surprise', whose name is known and always the same, points to something that cannot be interpreted as string[][] , and this leads to exception in Json.NET.

Is there any way to make Json.NET ignore specific dictionary key?

You can introduce a custom generic JsonConverter for IDictionary<string, TValue> that filters out invalid dictionary values (ie those that cannot be deserialized successfully to the dictionary value type):

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;
        }
    }
}

Then you can add it to settings as follows:

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

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

Or add it directly to ResultView with JsonConverterAttribute :

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

Notes:

  • I wrote the converter in a general way to handle any type of dictionary value including primitives such as int or DateTime as well as arrays or objects.

  • While a JSON file with invalid dictionary values (ones that cannot be deserialized to the dictionary value type) should be deserializable, an ill-formed JSON file (eg one that is truncated) should still result in an exception being thrown.

    The converter handles this by first loading the value into a JToken then attempting to deserialize the token. If the file is ill-formed, JToken.Load(reader) will throw an exception, which is intentionally not caught.

  • Json.NET's exception handling is reported to be "very flaky" (see eg Issue #1580: Regression from Json.NET v6: cannot skip an invalid object value type in an array via exception handling ) so I did not rely on it to skip invalid dictionary values.

  • I'm not 100% sure I got all cases of comment handling correct. So that may need additional testing.

Working sample .Net fiddle here .

I think you could ignore exceptions like this:

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 would contain the actual error message.

args.ErrorContext.Handled = true; will tell Json.Net to proceed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM