Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serializing a custom subclass of NameValueCollection with Json.Net

Tags:

json.net

I have the following class I am unsuccessfully attempting to serialize to Json.

class HL7 : NameValueCollection
{
  public List<HL7> Children { get; set; }
  public HL7()
  {
    Children = new List<HL7>();
  }
}

I have created the object like so and added data to it:

HL7 hl7 = new HL7();
hl7.Add("a", "123");
hl7.Add("b", "456");
hl7.Children.Add(new HL7());
hl7.Children[0].Add("c", "123");
hl7.Children[0].Add("d", "456");

When I call

JsonConvert.SerializeObject(hl7)

I receive

["a","b"]

I was expecting the following:

{
  "a": "123",
  "b": "456",
  "Children": [
    {
      "c": "123",
      "d": "456",
    }
  ]
} 
like image 773
user481779 Avatar asked Feb 24 '15 19:02

user481779


4 Answers

There are a few things going on here:

  1. Json.NET cannot serialize a NameValueCollection without a custom converter because NameValueCollection implements IEnumerable for iterating over the keys, but does not implement IDictionary for iterating over keys and values. See this answer for a fuller explanation of why this causes problems for Json.NET.

  2. Because NameValueCollection implements IEnumerable, Json.NET sees your class as a collection, and so serializes it as a JSON array and not a JSON object with named properties. Thus, your Children are not serialized. Again, a custom converter would be required to fix this.

  3. Assuming the above issues are resolved, if your HL7 subclass of NameValueCollection happens to have a key named "Children" you will generate invalid JSON when serializing it, namely an object with duplicated property names. I suggest moving the names & values into a nested property (named, e.g., "Values") for purposes of unambiguous serialization.

  4. NameValueCollection actually can have multiple string values for a given key string, so its entry values need to be serialized as a JSON array not a single string.

Putting all this together, the following code:

[JsonConverter(typeof(HL7Converter))]
public class HL7 : NameValueCollection
{
    public List<HL7> Children { get; set; }
    public HL7()
    {
        Children = new List<HL7>();
    }
}

public class HL7Converter : JsonConverter
{
    class HL7Proxy
    {
        public NameValueCollectionDictionaryWrapper Values { get; set; }
        public List<HL7> Children { get; set; }
    }


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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var proxy = serializer.Deserialize<HL7Proxy>(reader);
        if (proxy == null)
            return existingValue;
        var hl7 = existingValue as HL7;
        if (hl7 == null)
            hl7 = new HL7();
        hl7.Add(proxy.Values.GetCollection());
        if (proxy.Children != null)
            hl7.Children.AddRange(proxy.Children);
        return hl7;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        HL7 hl7 = (HL7)value;
        if (hl7 == null)
            return;

        serializer.Serialize(writer, new HL7Proxy { Children = hl7.Children, Values = new NameValueCollectionDictionaryWrapper(hl7) });
    }
}

// Proxy dictionary to serialize & deserialize a NameValueCollection.  We use a proxy dictionary rather than a real dictionary because NameValueCollection is an ordered collection but the generic dictionary class is unordered.
public class NameValueCollectionDictionaryWrapper: IDictionary<string, string []>
{
    readonly NameValueCollection collection;

    public NameValueCollectionDictionaryWrapper()
        : this(new NameValueCollection())
    {
    }

    public NameValueCollectionDictionaryWrapper(NameValueCollection collection)
    {
        this.collection = collection;
    }

    // Method instead of a property to guarantee that nobody tries to serialize it.
    public NameValueCollection GetCollection()
    {
        return collection;
    }

    #region IDictionary<string,string[]> Members

    public void Add(string key, string[] value)
    {
        if (collection.GetValues(key) != null)
            throw new ArgumentException("Duplicate key " + key);
        foreach (var str in value)
            collection.Add(key, str);
    }

    public bool ContainsKey(string key)
    {
        return collection.GetValues(key) != null;
    }

    public ICollection<string> Keys
    {
        get {
            return collection.AllKeys;
        }
    }

    public bool Remove(string key)
    {
        bool found = ContainsKey(key);
        if (found)
            collection.Remove(key);
        return found;
    }

    public bool TryGetValue(string key, out string[] value)
    {
        value = collection.GetValues(key);
        return value != null;
    }

    public ICollection<string[]> Values
    {
        get {
            return Enumerable.Range(0, collection.Count).Select(i => collection.GetValues(i)).ToArray();
        }
    }

    public string[] this[string key]
    {
        get
        {
            var value = collection.GetValues(key);
            if (value == null)
                throw new KeyNotFoundException();
            return value;
        }
        set
        {
            Remove(key);
            Add(key, value);
        }
    }

    #endregion

    #region ICollection<KeyValuePair<string,string[]>> Members

    public void Add(KeyValuePair<string, string[]> item)
    {
        Add(item.Key, item.Value);
    }

    public void Clear()
    {
        collection.Clear();
    }

    public bool Contains(KeyValuePair<string, string[]> item)
    {
        string [] value;
        if (!TryGetValue(item.Key, out value))
            return false;
        return EqualityComparer<string[]>.Default.Equals(item.Value, value); // Consistent with Dictionary<TKey, TValue>
    }

    public void CopyTo(KeyValuePair<string, string[]>[] array, int arrayIndex)
    {
        foreach (var item in this)
            array[arrayIndex++] = item;
    }

    public int Count
    {
        get { return collection.Count; }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(KeyValuePair<string, string[]> item)
    {
        if (Contains(item))
            return Remove(item.Key);
        return false;
    }

    #endregion

    #region IEnumerable<KeyValuePair<string,string[]>> Members

    public IEnumerator<KeyValuePair<string, string[]>> GetEnumerator()
    {
        foreach (string key in collection)
        {
            yield return new KeyValuePair<string, string[]>(key, collection.GetValues(key)); 
        }
    }

    #endregion

    #region IEnumerable Members

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion
}

Using the following test case:

        HL7 hl7 = new HL7();
        hl7.Add("a", "123");
        hl7.Add("b", "456");
        hl7.Add("Children", "Children");
        hl7.Children.Add(new HL7());
        hl7.Children[0].Add("c", "123");
        hl7.Children[0].Add("d", "456");
        hl7.Children[0].Add("d", "789");

        var json = JsonConvert.SerializeObject(hl7, Formatting.Indented);

        Debug.WriteLine(json);

Gives the following JSON:

{
  "Values": {
    "a": [
      "123"
    ],
    "b": [
      "456"
    ],
    "Children": [
      "Children"
    ]
  },
  "Children": [
    {
      "Values": {
        "c": [
          "123"
        ],
        "d": [
          "456",
          "789"
        ]
      },
      "Children": []
    }
  ]
}
like image 198
dbc Avatar answered Nov 13 '22 21:11

dbc


Inspired by this answer how to convert NameValueCollection to JSON string? , here is the working code (the only bad part is probably the "Children" string that is the property name. If you'll do a refactor, this will cause an error.

JsonConvert.SerializeObject(NvcToDictionary(hl7, false));

And the function:

static Dictionary<string, object> NvcToDictionary(HL7 nvc, bool handleMultipleValuesPerKey)
    {
        var result = new Dictionary<string, object>();
        foreach (string key in nvc.Keys)
        {
            if (handleMultipleValuesPerKey)
            {
                string[] values = nvc.GetValues(key);
                if (values.Length == 1)
                {
                    result.Add(key, values[0]);
                }
                else
                {
                    result.Add(key, values);
                }
            }
            else
            {
                result.Add(key, nvc[key]);
            }
        }


        if (nvc.Children.Any())
        {
            var listOfChildrenDictionary = new List<Dictionary<string, object>>();
            foreach (var nvcChildren in nvc.Children){
                listOfChildrenDictionary.Add(NvcToDictionary(nvcChildren, false));
            }

            result.Add("Children", listOfChildrenDictionary);
        }

        return result;
    }
like image 30
Alessio Avatar answered Nov 13 '22 20:11

Alessio


I have had issues with serializing NameValueCollections, using JSON.Net, The only way I have found is to convert it to a dictionary and then serialize it like:

var jsonString = JsonConvert.SerializeObject(new
{
    Parent = hl7.AllKeys.ToDictionary(r => r, r => hl7[r]),
    Children = hl7.Children.Select(c => c.AllKeys.ToDictionary(sub => sub, sub => c[sub]))
}, Newtonsoft.Json.Formatting.Indented);

and you will end up with:

{
  "Parent": {
    "a": "123",
    "b": "456"
  },
  "Children": [
    {
      "c": "123",
      "d": "456"
    }
  ]
}

But this will return "Parent" as well for top level items, since you have to specify a name for property in anonymous type

like image 25
Habib Avatar answered Nov 13 '22 21:11

Habib


Here's a custom serializer that will write the JSON as you were looking for, example program attached. The serializer is at the bottom. Note that you will need to add this converter to the JSON serializer settings, either through the default as I've done, or through the constructor of your serializer. Alternately, since you have a subclass you can use the JsonConverterAttribute on the HL7 class to assign the serializer

 public class Program
   {
      static int Main(string[] args) {
         JsonConvert.DefaultSettings = () => new JsonSerializerSettings {
            Converters = new []{ new HL7Converter() }
         };

         HL7 hl7 = new HL7();
         hl7.Add("a", "123");
         hl7.Add("b", "456");
         hl7.Children.Add(new HL7());
         hl7.Children[0].Add("c", "123");
         hl7.Children[0].Add("d", "456");

         Console.WriteLine (JsonConvert.SerializeObject (hl7));
         return 0;
      }
   }

   public class HL7 : NameValueCollection
   {
      public List<HL7> Children { get; set; }
      public HL7()
      {
         Children = new List<HL7> ();
      }
   }

   public class HL7Converter : Newtonsoft.Json.JsonConverter {
      #region implemented abstract members of JsonConverter

      public override void WriteJson (Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
      {
         var collection = (HL7)value;

         writer.WriteStartObject ();
         foreach (var key in collection.AllKeys) {
            writer.WritePropertyName (key);
            writer.WriteValue (collection [key]);
         }
         writer.WritePropertyName ("Children");
         serializer.Serialize (writer,collection.Children);
         writer.WriteEndObject ();
      }

      public override object ReadJson (Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
      {
         HL7 collection = existingValue as HL7 ?? new HL7 ();
         JObject jObj = JObject.Load (reader);
         foreach (var prop in jObj.Properties()) {
            if (prop.Name != "Children") {
               collection.Add (prop.Name, prop.Value.ToObject<string> ());
            } else {
               collection.Children = jObj.ToObject<List<HL7>> ();
            }
         }
         return collection;
      }

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

      #endregion
   }
like image 1
Steve Mitcham Avatar answered Nov 13 '22 21:11

Steve Mitcham