Serializing a custom subclass of NameValueCollection with Json.Net
There are a few things going on here:
Json.NET cannot serialize a
NameValueCollection
without a custom converter becauseNameValueCollection
implementsIEnumerable
for iterating over the keys, but does not implementIDictionary
for iterating over keys and values. See this answer for a fuller explanation of why this causes problems for Json.NET.Because
NameValueCollection
implementsIEnumerable
, 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, yourChildren
are not serialized. Again, a custom converter would be required to fix this.Assuming the above issues are resolved, if your
HL7
subclass ofNameValueCollection
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.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": []
}
]
}
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;
}