1

I am trying to create a custom JsonConverter to follow a third party API with a rather complicated object structure, and am a bit hamstrung on a few things. Note I am using .NET 4.7.2, not Core.

I have source objects that look like this, which I have created as objects rather than dictionaries in order to easily integrate them with FluentValidation. The objects will represent json objects in the foreign API. The "Value" is an interfaced item which is eventually converted to one of about 20 concrete types.

public class PDItem : IDictionaryObject<List<IItem>>
    {
        [JsonProperty]
        public string Key { get; set; }

        [JsonProperty]
        public List<IItem> Value { get; set; }
    }

The source json for such an object is a dictionary, whereby the Key is the property, and the value is one of the 20 object types, as so:

{
    "checked": [
            {
                "elementType": "input",
                "inputType": "checkbox",
                "name": "another_checkbox",
                "label": "Another checkbox",
                "checked": true
            }
    ]
}

In this case, the object in C# would look something like this:

PDItem item = new PDItem() {
    Key = "checked",
    Value = new List<IItem>() {
        new ConcreteTypeA () {
            elementType = "input",
            inputType = "checkbox",
            name = "another_checkbox",
            label = "Another checkbox",
            @checked = true
        }
    }
};

For reference, my "PDItem"s implement the following interfaces:

public interface IDictionaryObject<T>
    {
        string Key { get; set; }
        T Value { get; set; }
    }
[JsonConverter(typeof(IItemConverter))]
    public interface IItem: IElement
    {
    }
[JsonConverter(typeof(ElementConverter))]
    public interface IElement
    {
        string elementType { get; }
    }

I was able to convert some concrete types (without having to do any tricky dictionary to object conversions), below is a working example of the ElementConverter attacked to my IElement interface (IItem uses the same pattern, and the same JsonCreationConverter class):

public class ElementConverter : JsonCreationConverter<IElement>
{
    protected override IElement Create(Type objectType, JObject jObject)
    {
        //TODO:  Add objects to ElementConverter as they come online.
        switch (jObject["elementType"].Value<string>())
        {
            case ElementTypeDescriptions.FirstType:
                return new FirstType();
            case ElementTypeDescriptions.SecondType:
                return new SecondType();
            case ElementTypeDescriptions.ThirdType:
                return new ThirdType();
            case ElementTypeDescriptions.FourthType:
            case ElementTypeDescriptions.FifthType:
            default:
                throw new NotImplementedException("This object type is not yet implemented.");
        }
    }
}

public abstract class JsonCreationConverter<T> : JsonConverter
{
    protected abstract T Create(Type objectType, JObject jObject);

    public override bool CanConvert(Type objectType)
    {
        return typeof(T) == objectType;
    }

    public override object ReadJson(JsonReader reader, Type objectType,
        object existingValue, JsonSerializer serializer)
    {
        try
        {
            var jObject = JObject.Load(reader);
            var target = Create(objectType, jObject);
            serializer.Populate(jObject.CreateReader(), target);
            return target;
        }
        catch (JsonReaderException)
        {
            return null;
        }
    }

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

What makes my scenario so tricky is not that I am converting objects to dictionaries and back, but that my object values are other concrete objects (interfaced). I want to write a custom JsonConverter to serialize and deserialize these objects, but have no idea how to read and write the json in the methods below, let alone if what I am attempting to do is even possible. Any assistance would be greatly appreciated!

public class PDItemConverter: JsonConverter<PDItem>
    {
        public override void WriteJson(JsonWriter writer, PDItem value, JsonSerializer serializer)
        {
            /// DO STUFF
        }

        public override PDItem ReadJson(JsonReader reader, Type objectType, PDItem existingValue,
            bool hasExistingValue, JsonSerializer serializer)
        {
            /// DO STUFF
        }
    }

EDITED PER DBC's request: Apologies for the complicated question DBC, and I greatly appreciate your time! Obviously, I'm a bit new to posting to stack overflow (long time lurker as they say). Below is full working code that will run in .net fiddle (or in a console application if you simply add a namespace and json.net 12.x packages to a new .NET 4.7.2 console project). Second apologies that it is still a bit long and complicated, It is actually greatly simplified thanks to the omission of the Validation code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class PDItem : IDictionaryObject<List<IItem>>
    {
        [JsonProperty]
        public string Key { get; set; }

        [JsonProperty]
        public List<IItem> Value { get; set; }
    }

    public interface IDictionaryObject<T>
    {
        string Key { get; set; }
        T Value { get; set; }
    }

    [JsonConverter(typeof(IItemConverter))]
    public interface IItem : IElement
    {
        string itemType { get; }
    }

    [JsonConverter(typeof(ElementConverter))]
    public interface IElement
    {
        string elementType { get; }
    }

    public class ElementConverter : JsonCreationConverter<IElement>
    {
        protected override IElement Create(Type objectType, JObject jObject)
        {
            //TODO:  Add objects to ElementConverter as they come online.
            switch (jObject["elementType"].Value<string>())
            {
                case ElementTypeDescriptions.FirstType:
                    return new FirstType();
                case ElementTypeDescriptions.SecondType:
                    return new SecondType();
                //case ElementTypeDescriptions.ThirdType:
                //    return new ThirdType();
                //case ElementTypeDescriptions.FourthType:
                //case ElementTypeDescriptions.FifthType:
                default:
                    throw new NotImplementedException("This object type is not yet implemented.");
            }
        }
    }

    public class IItemConverter : JsonCreationConverter<IItem>
    {
        protected override IItem Create(Type objectType, JObject jObject)
        {
            switch (jObject["itemType"].Value<string>())
            {
                case ItemTypeDescriptions.FirstItemType:
                    return new FirstItemType();
                case ItemTypeDescriptions.SecondItemType:
                    return new SecondItemType();
                default:
                    throw new NotImplementedException("This object type is not yet implemented.");
            }
        }
    }

    /// <summary>
    /// Used constants rather than an enum to allow for use in switch statements.  Provided by third party to us to identify their classes across the API.
    /// </summary>
    public class ElementTypeDescriptions
    {
        public const string FirstType = "firstTypeId";
        public const string SecondType = "secondTypeId";
        public const string ThirdType = "thirdTypeId";
        public const string FourthType = "fourthTypeId";
        public const string FifthType = "fifthTypeId";
    }

    /// <summary>
    /// Used constants rather than an enum to allow for use in switch statements.  Provided by third party to us to identify their classes across the API.
    /// </summary>
    public class ItemTypeDescriptions
    {
        public const string FirstItemType = "firstItemTypeId";
        public const string SecondItemType = "secondItemTypeId";
    }

    /*** CONCRETE OBJECTS ***/
    public class FirstType : IElement
    {
        public string elementType { get { return ElementTypeDescriptions.FirstType; } }
        public string name { get; set; }
    }

    public class SecondType : IElement
    {
        public string elementType { get { return ElementTypeDescriptions.FirstType; } }
        public string label { get; set; }
    }

    public class FirstItemType : IItem
    {
        public string elementType { get { return ElementTypeDescriptions.FourthType; } }
        public string itemType { get { return ItemTypeDescriptions.FirstItemType; } }
        public string reference { get; set; }
    }

    public class SecondItemType : IItem
    {
        public string elementType { get { return ElementTypeDescriptions.FourthType; } }
        public string itemType { get { return ItemTypeDescriptions.FirstItemType; } }
        public string database { get; set; }
    }
    /*** END CONCRETE OBJECTS ***/

    public class PDItemConverter : JsonConverter<PDItem>
    {
        public override void WriteJson(JsonWriter writer, PDItem value, JsonSerializer serializer)
        {
            /// THIS CODE TO BE WRITTEN TO ANSWER THE QUESTION
            /// DO STUFF
            throw new NotImplementedException("THIS CODE TO BE WRITTEN TO ANSWER THE QUESTION");
        }

        public override PDItem ReadJson(JsonReader reader, Type objectType, PDItem existingValue,
            bool hasExistingValue, JsonSerializer serializer)
        {
            /// THIS CODE TO BE WRITTEN TO ANSWER THE QUESTION
            /// DO STUFF
            throw new NotImplementedException("THIS CODE TO BE WRITTEN TO ANSWER THE QUESTION");
        }
    }

    public abstract class JsonCreationConverter<T> : JsonConverter
    {
        protected abstract T Create(Type objectType, JObject jObject);

        public override bool CanConvert(Type objectType)
        {
            return typeof(T) == objectType;
        }

        public override object ReadJson(JsonReader reader, Type objectType,
            object existingValue, JsonSerializer serializer)
        {
            try
            {
                var jObject = JObject.Load(reader);
                var target = Create(objectType, jObject);
                serializer.Populate(jObject.CreateReader(), target);
                return target;
            }
            catch (JsonReaderException)
            {
                return null;
            }
        }

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

    class TestClass
    {
        public static void Test()
        {
            var json = GetJson();

            var item = JsonConvert.DeserializeObject<PDItem>(json);

            var json2 = JsonConvert.SerializeObject(item, Formatting.Indented);

            Console.WriteLine(json2);
        }

        static string GetJson()
        {
            var json = @"{
                            ""checked"": [
                                {
                                    ""elementType"": ""input"",
                                    ""inputType"": ""checkbox"",
                                    ""name"": ""another_checkbox"",
                                    ""label"": ""Another checkbox"",
                                    ""checked"": true
                                }   
                            ]
                        }
                    ";
            return json;
        }
    }

    public class Program
    {
        public static void Main()
        {
            Console.WriteLine("Environment version: " + Environment.Version);
            Console.WriteLine("Json.NET version: " + typeof(JsonSerializer).Assembly.FullName);
            Console.WriteLine();

            try
            {
                TestClass.Test();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed with unhandled exception: ");
                Console.WriteLine(ex);
                throw;
            }
        }
    }
christopmj
  • 13
  • 4
  • Your question is complex so I tried to create a extract a [mcve] showing the problem: https://dotnetfiddle.net/DSDuq7. However, your code doesn't compile, because there is no `IItemConverter`, no `ElementTypeDescriptions`, no `FirstType`, no `SecondType`, and so on included in the question. Might you please simplify your question so that everything other than `PDItemConverter` is provided, so we can test it ourselves? See [ask]. – dbc Jul 22 '19 at 20:23
  • DBC, I've created a codebase from your fiddler including the missing classes and posted the full contents above. Am a bit pressed for time at this exact moment, but I've taken your advice to heart, and will follow those guidelines in the future. Thanks again for the time you've taken to look at my problem and advise me! – christopmj Jul 23 '19 at 13:01

1 Answers1

2

One of the easiest ways to write a JsonConverter is to map the object to be serialized to some DTO, then (de)serialize the DTO. Since your PDItem looks like a single dictionary key/value pair and is serialized like a dictionary, the easiest DTO to use would be an actual dictionary, namely Dictionary<string, List<IItem>>.

Thus your PDItemConverter can be written as follows:

public class PDItemConverter: JsonConverter<PDItem>
{
    public override void WriteJson(JsonWriter writer, PDItem value, JsonSerializer serializer)
    {
        // Convert to a dictionary DTO and serialize
        serializer.Serialize(writer, new Dictionary<string, List<IItem>> { { value.Key, value.Value } });
    }

    public override PDItem ReadJson(JsonReader reader, Type objectType, PDItem existingValue,
        bool hasExistingValue, JsonSerializer serializer)
    {
        // Deserialize as a dictionary DTO and map to a PDItem
        var dto = serializer.Deserialize<Dictionary<string, List<IItem>>>(reader);
        if (dto == null)
            return null;
        if (dto.Count != 1)
            throw new JsonSerializationException(string.Format("Incorrect number of dictionary keys: {0}", dto.Count));
        var pair = dto.First();
        existingValue = hasExistingValue ? existingValue : new PDItem();
        existingValue.Key = pair.Key;
        existingValue.Value = pair.Value;
        return existingValue;
    }
}

Since you are (de)serializing using the incoming serializer any converters associated to nested types such as IItem will get picked up and used automatically.

In addition, in JsonCreationConverter<T> you need to override CanWrite and return false. This causes the serializer to fall back to default serialization when writing JSON as explained in this answer to How to use default serialization in a custom JsonConverter. Also, I don't recommend catching and swallowing JsonReaderException. This exception is thrown when the JSON file itself is malformed, e.g. by being truncated. Ignoring this exception and continuing can occasionally force Newtonsoft to fall into an infinite loop. Instead, propagate the exception up to the application:

public abstract class JsonCreationConverter<T> : JsonConverter
{
    // Override CanWrite and return false
    public override bool CanWrite { get { return false; } }

    protected abstract T Create(Type objectType, JObject jObject);

    public override bool CanConvert(Type objectType)
    {
        return typeof(T) == objectType;
    }

    public override object ReadJson(JsonReader reader, Type objectType,
        object existingValue, JsonSerializer serializer)
    {
        var jObject = JObject.Load(reader);
        var target = Create(objectType, jObject);
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

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

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    This is exactly what I was confused about, the actual implementation of the custom ReadJson/WriteJson. Bonus points for pointing out the hidden pitfall I had with "CanWrite". I had not overridden that to return false because I did not realize NewtonSoft would revert to standard deserialization. In the past, my object models have been straight up simple DTO, as I generally believe in keeping DTO models as close to the source structrure as possible, but in this case Dictionaries are just too difficult to integrate with FluentValidation, custom JsonConverters were comparatively easy. – christopmj Jul 23 '19 at 13:31