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",
}
]
}
There are a few things going on here:
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.
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.
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.
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;
}
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
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With