Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Json.NET - Default deserialization behavior for a single property in CustomCreationConverter

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.

like image 843
Tim Avatar asked Feb 03 '14 21:02

Tim


People also ask

How do I deserialize a JSON object?

Implement deserialization by doing the following steps in the Read () method: Parse JSON into a JsonDocument. Get the properties needed to call the parameterized constructor. Create the object with the parameterized constructor. Set the rest of the properties.

What are the default behaviors for jsonserializeroptions in ASP NET Core?

When you use System.Text.Json indirectly in an ASP.NET Core app, some default behaviors are different. For more information, see Web defaults for JsonSerializerOptions. By default, all public properties are serialized. You can specify properties to ignore. By default, JSON is minified. You can pretty-print the JSON.

Are JSON properties serialized or minified?

By default, all public properties are serialized. You can specify properties to ignore. The default encoder escapes non-ASCII characters, HTML-sensitive characters within the ASCII-range, and characters that must be escaped according to the RFC 8259 JSON spec. By default, JSON is minified.

What is customcreationconverter in JSON?

CustomCreationConverter. The CustomCreationConverter<T> is a JsonConverter that provides a way to customize how an object is created during JSON deserialization. Once the object has been created it will then have values populated onto it by the serializer.


1 Answers

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
like image 111
Brian Rogers Avatar answered Nov 15 '22 22:11

Brian Rogers