3

I have a couple .NET classes that get generated that go three levels deep and I'd like to serialize them in a special format. So, I began writing a custom Json Serializer using Newtonsoft.Json.

I believe it will be difficult to fully explain, so I've posted the code along with the goal here: https://dotnetfiddle.net/CDGcMW

Essentially, there is an initial array that would contain objects and there would be properties for that object. The difficult part is that these properties are not known, thus is why I'm attempting to create a custom serializer.

Any help in determining how I can make the Json produced here https://dotnetfiddle.net/CDGcMW become the "goal" JSON that's commented out would be greatly appreciated.

Edit: Updated dotnetfiddle to a smaller example. Original is here: https://dotnetfiddle.net/dprfDu

AJ Tatum
  • 653
  • 2
  • 15
  • 35
  • Not really sure I understand the question. Are you looking to exclude properties of a type when deeply nested in an object graph, but not when at the top of the graph? If so maybe [Json.NET serialize by depth and attribute](https://stackoverflow.com/q/36159424/3744182) would help. – dbc Dec 14 '17 at 00:30
  • If that link doesn't help, is there any way you can simplify your fiddle into a [mcve] that shows the current output and required output with as few extraneous fields and properties as possible? – dbc Dec 14 '17 at 00:38
  • Thank you, I've updated the example to be shorter and I'll look into the IContractResolver. – AJ Tatum Dec 14 '17 at 00:48
  • Sorry, forgot to save. – AJ Tatum Dec 14 '17 at 01:03
  • Would it be correct and sufficient to state that you want to use a different conversion logic when an instance of your type is (directly or indirectly) nested inside another instance of your type? – dbc Dec 14 '17 at 18:45
  • That's correct @dbc – AJ Tatum Dec 15 '17 at 05:15
  • So you'd recommend creating a new class that perhaps inherits the DataMapper class but has a different JsonSerializer? – AJ Tatum Dec 15 '17 at 05:41
  • [net fiddle example](https://dotnetfiddle.net/VSbrJy) without any custom formatters – Sir Rufo Dec 20 '17 at 12:47

2 Answers2

5

Your "goal" JSON is tricky to handle because the treatment of the SubDataMappers list is different depending on whether the children have a non-null DataMapperProperty or a non-empty list of SubDataMappers. In the former case, you want it rendered as an object containing one property per child DataMapper; in the latter, as an array of objects containing one DataMapper each. Also, I see you are using the Name property of the DataMapper as a key in the JSON rather than as the value of a well-known property. Given these two constraints, I think the best plan of attack is to make a JsonConverter that operates on a list of DataMappers rather than a single instance. Otherwise, the converter code is going to get pretty messy. If that is acceptable, then the following converter should give you what you want:

public class DataMapperListConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(List<DataMapper>);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        List<DataMapper> list = (List<DataMapper>)value;
        if (list.Any(dm => dm.DataMapperProperty != null))
        {
            JObject obj = new JObject(list.Select(dm =>
            {
                JToken val;
                if (dm.DataMapperProperty != null)
                    val = JToken.FromObject(dm.DataMapperProperty, serializer);
                else 
                    val = JToken.FromObject(dm.SubDataMappers, serializer);
                return new JProperty(dm.Name, val);
            }));
            obj.WriteTo(writer);
        }
        else
        {
            serializer.Serialize(writer,
                list.Select(dm => new Dictionary<string, List<DataMapper>>
                {
                    { dm.Name, dm.SubDataMappers }
                }));
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Object)
        {
            return token.Children<JProperty>()
                 .Select(jp => 
                 {
                     DataMapper mapper = new DataMapper { Name = jp.Name };
                     JToken val = jp.Value;
                     if (val["data-type"] != null)
                         mapper.DataMapperProperty = jp.Value.ToObject<DataMapperProperty>(serializer);
                     else
                         mapper.SubDataMappers = jp.Value.ToObject<List<DataMapper>>(serializer);
                     return mapper;
                 })
                 .ToList();
        }
        else if (token.Type == JTokenType.Array)
        {
            return token.Children<JObject>()
                .SelectMany(jo => jo.Properties())
                .Select(jp => new DataMapper
                {
                    Name = jp.Name,
                    SubDataMappers = jp.Value.ToObject<List<DataMapper>>(serializer)
                })
                .ToList();
        }
        else
        {
            throw new JsonException("Unexpected token type: " + token.Type.ToString());
        }
    }
}

Assumptions:

  • You will never be serializing a single DataMapper by itself; it will always be contained in a list.
  • DataMappers can be nested to an arbitrary depth.
  • A DataMapper will always have a non-null Name, which is unique at each level.
  • A DataMapper will never have both a non-null DataMapperProperty and a non-empty list of SubDataMappers.
  • A DataMapperProperty will always have a non-null DataType.
  • A DataMapper will never have a Name of data-type.

If the last four assumptions do not hold true, then this JSON format will not work for what you are trying to do, and you will need to rethink.

To use the converter, you will need to add it to your serializer settings as shown below. Use the settings both when you serialize and deserialize. Remove the [JsonConverter] attribute from the DataMapper class.

var settings = new JsonSerializerSettings()
{
    Converters = new List<JsonConverter> { new DataMapperListConverter() },
    Formatting = Formatting.Indented
};

Here is a round-trip demo: https://dotnetfiddle.net/8KycXB

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • Thank you tremendously for your help. I appreciate it! After further development, there is a scenario where there is an object three levels deep, which wasn't in my original example. I forked your code and adjusted it to include an object three levels deep as that results in the object being null. Here is the code: https://dotnetfiddle.net/AFl36o. Thank you again so much, I'm learning a lot! – AJ Tatum Dec 19 '17 at 23:46
  • This is the WriteJson function I had, which is far messier than your solution and I'm not getting the ReadJson working. https://dotnetfiddle.net/fAKOrK. Any help to make your solution support three levels deep would be incredible! – AJ Tatum Dec 20 '17 at 03:36
  • I see the reason that level 3 is coming out null. You now have a heterogeneous list of SubDataMappers at level two: two of the items have DataMapperProperties and the new one has a list of SubMappers instead. My converter was expecting all of one or all of the other, as it was relying on that as a means to determine whether to render the list as an array or as an object. I can fix the converter to follow the format on serialization, but on deserialization this causes a new problem: (continued) – Brian Rogers Dec 20 '17 at 05:02
  • ... there is no longer a way to tell whether a JSON object represents a DataMapper with a DataMapperProperty or one with a list of SubMappers. Level 3 will render as an object rather than an array because it contains a child with a DataMapperProperty. So, we need another way to tell the difference. Let me ask you this-- if a DataMapper has a DataMapperProperty, can I rely on the fact that it will always have a non-null `data-type` in it, and that none of the DataMappers will have a `Name` called `data-type`? If so, I think I can make it work. – Brian Rogers Dec 20 '17 at 05:09
  • Interesting,thank you for your explanation! Yes, the DataMapperProperty will always contain a non-null data-type (as well as source. There wouldn't be a scenario where the name would be called data-type as well.I sincerely appreciate your help! – AJ Tatum Dec 20 '17 at 05:16
  • OK, I have updated my answer. The converter should handle the heterogeneous list properly now. Code is here: https://dotnetfiddle.net/8KycXB. If you're not married to the arrays, it is also possible to simplify the JSON format such that it always uses JSON objects only. This will also simplify the code in the converter. Code for that is here: https://dotnetfiddle.net/qDGAW0 – Brian Rogers Dec 20 '17 at 05:36
0

You can achieve the deep nested serialization with JSON.NET by replacing all your typed classes with ExpandoObject. It works for me. Let me know if it works for you or need any sample to show you.

UPDATE:

Here is the working sample

https://dotnetfiddle.net/jtebDs

Hope this is what you would like see the output. Let me know if you have any questions.

Cinchoo
  • 6,088
  • 2
  • 19
  • 34
  • Would that be working with @brian-rogers solution or something different? I have able to get a WriteJson function to work by creating this (messy, though working) code: https://dotnetfiddle.net/fAKOrK. Making a ReadJson function is proving to be difficult though. If you could point me towards an example/sample I'd appreciate it. – AJ Tatum Dec 20 '17 at 03:38