Json.NET - Default deserialization behavior for a

2020-07-06 03:34发布

问题:

In the following scenario, how do I get CrazyItemConverter to carry on as usual when it encounters a JSON property that exists in the type I'm deserializing to?

I have some JSON that looks like this:

{
    "Item":{
        "Name":"Apple",
        "Id":null,
        "Size":5,
        "Quality":2
    }
}

The JSON gets deserialized into a class that looks a whole lot like this:

[JsonConverter(typeof(CrazyItemConverter))]
public class Item
{
    [JsonConverter(typeof(CrazyStringConverter))]
    public string Name { get; set; }

    public Guid? Id { get; set; }

    [JsonIgnore]
    public Dictionary<string, object> CustomFields
    {
        get
        {
            if (_customFields == null)
                _customFields = new Dictionary<string, object>();
            return _customFields;
        }
    }

    ...
}

CrazyItemConverter populates the values of the known properties and puts the unknown properties in CustomFields. The ReadJson in it looks like this:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    var outputObject = Create(objectType);
    var objProps = objectType.GetProperties().Select(p => p.Name).ToArray();

    while (reader.Read())
    {
        if (reader.TokenType == JsonToken.PropertyName)
        {
            string propertyName = reader.Value.ToString();
            if (reader.Read())
            {
                if (objProps.Contains(propertyName))
                {
                    // No idea :(
                    // serializer.Populate(reader, outputObject);
                }
                else
                {
                    outputObject.AddProperty(propertyName, reader.Value);
                }
            }
        }
    }
    return outputObject;
}

During deserialization, when CrazyItemConverter encounters a known property, I want it to act as it normally would. Meaning, respecting the [JsonConverter(typeof(CrazyStringConverter))] for Name.

I was using the code below to set the known properties but, it throws exceptions on nullables and doesn't respect my other JsonConverters.

PropertyInfo pi = outputObject.GetType().GetProperty(readerValue, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
var convertedValue = Convert.ChangeType(reader.Value, pi.PropertyType);
pi.SetValue(outputObject, convertedValue, null);

Any ideas?

UPDATE: I've learned that serializer.Populate(reader, outputObject); is how to deserialize the whole thing but it doesn't seem to work if you want default functionality on a property-by-property basis.

回答1:

If I'm understanding correctly, your CrazyItemConverter exists so that you can deserialize known properties in the JSON to strongly-typed properties, while still preserving "extra" fields that may be in the JSON into dictionary.

It turns out that Json.Net already has this feature built in (since 5.0 release 5), so you don't need a crazy converter. Instead, you just need to mark your dictionary with the [JsonExtensionData] attribute. (See the author's blog for more information.)

So your Item class would look like this:

public class Item
{
    [JsonConverter(typeof(CrazyStringConverter))]
    public string Name { get; set; }

    public Guid? Id { get; set; }

    [JsonExtensionData]
    public Dictionary<string, object> CustomFields
    {
        get
        {
            if (_customFields == null)
                _customFields = new Dictionary<string, object>();
            return _customFields;
        }
        private set
        {
            _customFields = value;
        }
    }
    private Dictionary<string, object> _customFields;
}

Then you can just deserialize it as normal. Demo:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        {
            ""Item"":
            {
                ""Name"":""Apple"",
                ""Id"":""4b7e9f9f-7a30-4f79-8e47-8b50ea26ddac"",
                ""Size"":5,
                ""Quality"":2
            }
        }";

        Item item = JsonConvert.DeserializeObject<Wrapper>(json).Item;
        Console.WriteLine("Name: " + item.Name);
        Console.WriteLine("Id: " + item.Id);
        foreach (KeyValuePair<string, object> kvp in item.CustomFields)
        {
            Console.WriteLine(kvp.Key + ": " + kvp.Value);
        }
    }
}

public class Wrapper
{
    public Item Item { get; set; }
}

class CrazyStringConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(string);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        // Reverse the string just for fun
        return new string(token.ToString().Reverse().ToArray());
    }

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

Output:

Name: elppA
Id: 4b7e9f9f-7a30-4f79-8e47-8b50ea26ddac
Size: 5
Quality: 2