4

I'm having a hard time overriding the WriteJson method of a custom JsonConverter in order to slightly alter the way serialization is performed.

I need to call a REST service that accepts a certain input that has a generic portion. I can reproduce the problem I'm having with the following payload format :

public sealed class JsonField
{
    public string Key { get; set; }
    public object Value { get; set; }
    public string OtherParam { get; set; }
}
public sealed class JsonPayload
{
    public string Item1 { get; set; }
    public string Item2 { get; set; }
    public List<JsonField> Fields { get; set; }
}

The REST API I'm calling needs to have Fields be an object containing as many fields with names corresponding to the Key properties specified in the original collection. Like so:

{
"Item1" : "Value1",
"Item2" : "Value2",
...
"Fields":
{
    "Key1": {"Value":"Value1", "OtherParam":"other1"}
    "Key2": {"Value":42, "OtherParam":"other2"}
}
}

However, using the default options, the payload is serialized like this:

{
"Item1" : "Value1",
"Item2" : "Value2",
...
"Fields":[
    { "Key":"Key1", "Value":"Value1", "OtherParam":"other1" }
    { "Key":"Key2", "Value":42, "OtherParam":"other2" }
]
}

You notice that there is a collection of objects where I would like a single object. Also, I would like the Key names to be the names of individual properties in the Fields.

I have a hard time figuring out how to use the JToken, JProperty, JValue object in my custom converter. I have, in fact, never attempted such a thing so I find it hard to wrap my head around those concepts.

I have tried creating two custom converters, one at the scope of the class, in order to prevent the collection from being generated, and the second one at the scope of the collection, but so far without success.

Here is what I have tried:

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

        var key = token["Key"];
        if (key == null)
            throw new Exception("missing key");

        var propertyName = key.ToString();

        var json = new StringBuilder();
        json.Append("{");

        foreach (var child in token.Children())
        {
            var property = child as JProperty;
            if (property == null || property.Name == "Key")
                continue;

            var propName = property.Name;
            var propValue = JsonConvert.SerializeObject(property.Value);
            json.AppendFormat("\"{0}\": {1},", propName, propValue);
        }

        if (json.Length > 1)
            json.Remove(json.Length - 1, 1);
        json.Append("}");

        var newToken = JToken.Parse(json.ToString());
        var serializedObject = JsonConvert.SerializeObject(newToken);

        writer.WriteStartObject();
        writer.WritePropertyName(propertyName);
        writer.WriteToken(newToken.CreateReader());
        writer.WriteEndObject();
    }

Is there a way to perform what I want to achieve?

Maybe I'm approaching the problem the wrong way, so please, if you have easier alternatives, then by all means do not hesitate to share what you have in mind.

Dog Ears
  • 9,637
  • 5
  • 37
  • 54
Maxime Labelle
  • 3,609
  • 2
  • 27
  • 48

2 Answers2

3

You're on the right track. You only need a single converter here -- for the list of JsonField objects --and the code is actually very straightforward. This is all you need:

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

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JObject containerObj = new JObject();
        foreach (JsonField field in (List<JsonField>)value)
        {
            JObject itemObj = new JObject();
            itemObj.Add("Value", JToken.FromObject(field.Value));
            itemObj.Add("OtherParam", new JValue(field.OtherParam));
            containerObj.Add(field.Key, itemObj);
        }
        containerObj.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Here's a demo showing the converter in action:

class Program
{
    static void Main(string[] args)
    {
        JsonPayload payload = new JsonPayload
        {
            Item1 = "Value1",
            Item2 = "Value2",
            Fields = new List<JsonField>
            {
                new JsonField { Key = "Key1", Value = "Value1", OtherParam = "other1" },
                new JsonField { Key = "Key2", Value = 42, OtherParam = "other2" },
            }
        };

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.Converters.Add(new JsonFieldListConverter());
        settings.Formatting = Formatting.Indented;

        string json = JsonConvert.SerializeObject(payload, settings);
        Console.WriteLine(json);
    }
}

Output:

{
  "Item1": "Value1",
  "Item2": "Value2",
  "Fields": {
    "Key1": {
      "Value": "Value1",
      "OtherParam": "other1"
    },
    "Key2": {
      "Value": 42,
      "OtherParam": "other2"
    }
  }
}
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
0

Short answer:

You can do this simply by changing the List<T> to a Dictionary<T>

Answer with an example:

You can do this simply by changing the List<T> to a Dictionary<T> this will give you what appears to be a single object with the property names as the keys of the dictionary. If the class contains the key this will also be serialized, although you could [JsonIgnore] it but that would mean it wouldn't be populated during deserialization.

void Main()
{
    var pl = new JsonPayload() 
    {Item1 = "Value1",Item2 = "Value2"};

    var field1 = new JsonField() { Key = "Key1", Value = "Value1", OtherParam = "other1" };
    var field2 = new JsonField() { Key = "Key2", Value = "Value2", OtherParam = "other2" };

    pl.Fields = new Dictionary<string, JsonField>() { { field1.Key , field1},  { field2.Key, field2 }}; 

    string json = JsonConvert.SerializeObject(pl);
    JsonPayload pl2 = JsonConvert.DeserializeObject<JsonPayload>(json);

    string output = JsonConvert.SerializeObject(pl2);
    output.Dump();

}
public sealed class JsonField
{
    public string Key { get; set; }
    public object Value { get; set; }
    public string OtherParam { get; set; }
}
public sealed class JsonPayload
{
    public string Item1 { get; set; }
    public string Item2 { get; set; }
    public Dictionary<string, JsonField> Fields { get; set; }
}
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
Dog Ears
  • 9,637
  • 5
  • 37
  • 54