Nested nodes are not formatted correctly after implementing JsonConverter

老子叫甜甜 提交于 2019-12-24 16:29:50

问题


I have a list of objects that I want to serialize as JSON like below, since there is a complex type in this list. I want to change this complex type to Key/Value pairs where each Key is the name of a property in the type, and each Value is the corresponding value of that property. I've tried multiple solutions but none of them worked for me.

Here is the object structure

public class Metadata
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Data
{
    // If I change the type of Metadata to IList<IDictionary<string, object>>
    // custom converter won't catch it at all when I pass it to its constructor
    //public IList<IDictionary<string, object>> Metadata { get; set; }
    public IList<Metadata> Metadata { get; set; }
    public int Length { get; set; }
    public string Type { get; set; }
}

Here is my desired output with two entries in IList<Metadata>

{
    "Metadata": [{
            "Key": "FirstName",
            "Value": "ABC"
        },
        {
            "Key": "LastName",
            "Value": "XYZ"
        },
        {
            "Key": "FirstName",
            "Value": "DEF"
        },
        {
            "Key": "LastName",
            "Value": "MNL"
        }
    ],
    "Length": 25,
    "Type": "application/mp3"
}

I know that JsonSerializer does not change the face of the object by itself, so I tried to change it by implementing a custom JsonConverter:

public class KeyValue
{
    public string Key { get; set; }
    public string Value { get; set; }
}

class CustomMetadataConverter : JsonConverter
{
    private readonly Type[] _types;

    public CustomMetadataConverter(params Type[] types)
    {
        _types = types;
    }

    public override bool CanConvert(Type objectType)
    {
        return _types.Any(t => t == objectType);
    }

    // I've removed ReadJson and CanRead here to keep the question clear

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JToken token = JToken.FromObject(value);

        if (token.Type != JTokenType.Object)
            token.WriteTo(writer);
        else
        {
            JObject jsonObject = (JObject)token;

            IList<KeyValue> properties = jsonObject.Properties()
                .Select(p => new KeyValue { Key = p.Name, Value = p.Value.ToString() }).ToList();

            // If I change the IList<KeyValue> to anonymous array, output would be the same
            //var properties = jsonObject.Properties().Select(p => new { Key = p.Name, Value = p.Value.ToString() }).ToArray();

            jsonObject.RemoveAll();

            jsonObject.Add(new JProperty("Metadata", JToken.FromObject(properties)));
            jsonObject.WriteTo(writer);
        }
    }
}

And here is how I call it:

var serializedObject = JsonConvert.SerializeObject(listOfData, Formatting.Indented, new CustomMetadataConverter(typeof(Metadata)));

I tried this solution to move it to root, but the output of the custom converter is wrapped with the parent instead of replacing it. I know it's because the custom converter only reads children of Metadata but if I change CustomMetadataConverter(typeof(Metadata)) to CustomMetadataConverter(typeof(Data)) it converts the whole query to Key/Value pairs. And that's not what I want.

Here is the output after implementing custom converter

{
  "Metadata": [
    {
      "Metadata": [
        {
          "Key": "FirstName",
          "Value": "ABC"
        },
        {
          "Key": "LastName",
          "Value": "XYZ"
        }
      ]
    }
  ],
  "Length": 25,
  "Type": "application/mp3"
}

回答1:


If you have two or more Metadata items in the list, and you serialized them to key-value pair objects in a flat array like you described in your question, then it would be difficult to tell which key-value pairs belong together if you need to deserialize the JSON later. It would be better to use a two-dimensional array structure like this instead:

{
  "Metadata": [
    [
      {
        "Key": "FirstName",
        "Value": "ABC"
      },
      {
        "Key": "LastName",
        "Value": "XYZ"
      }
    ],
    [
      {
        "Key": "FirstName",
        "Value": "DEF"
      },
      {
        "Key": "LastName",
        "Value": "MNL"
      }
    ]
  ],
  "Length": 25,
  "Type": "application/mp3"
}

Here is a converter which will do that:

class ObjectToKvpArrayConverter<T> : JsonConverter where T : class
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JArray array = new JArray(
            JObject.FromObject(value)
                   .Properties()
                   .Select(jp => 
                       new JObject(
                           new JProperty("Key", jp.Name),
                           new JProperty("Value", jp.Value)
                       )
                   )
        );
        array.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject obj = new JObject(
            JArray.Load(reader)
                  .Children<JObject>()
                  .Select(jo => new JProperty((string)jo["Key"], jo["Value"]))
        );
        T result = Activator.CreateInstance<T>();
        serializer.Populate(obj.CreateReader(), result);
        return result;
    }
}

You can use the converter like this:

var json = JsonConvert.SerializeObject(data, Formatting.Indented, new ObjectToKvpArrayConverter<Metadata>());

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



来源:https://stackoverflow.com/questions/58568644/nested-nodes-are-not-formatted-correctly-after-implementing-jsonconverter

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!