Friday, August 8, 2014

JSON.NET Serialization of NameValueCollection

Yesterday, a colleague of mine was having problems implementing a Web API microservice, where client was throwing the exception:

'Cannot create and populate list type System.Collections.Specialized.NameValueCollection. Path ...' on de-serialization of a complex graph containing NameValueCollections.

Web searches returned a number suggestions to replace the NameValueCollection with a dictionary, which did not work in our case, where we needed to use classes as is.

As I've created the initial reference architecture, I felt obliged to help resolve the issue. As said, Web searches for a solution didn't return anything closely applicable, I've taken a bit deeper look at the Json.NET library documentation, and shortly afterwards decided to create a custom converter.

It turned out to be easy. Here's the source code:
/// <summary>
/// Custom converter for (de)serializing NameValueCollection
/// Add an instance to the settings Converters collection
/// </summary>
public class NameValueCollectionConverter : JsonConverter
{
  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    var collection = value as NameValueCollection;
    if (collection == null)
      return;

    writer.WriteStartObject();
    foreach (var key in collection.AllKeys)
    {
      writer.WritePropertyName(key);
      writer.WriteValue(collection.Get(key));
    }
    writer.WriteEndObject();
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    var nameValueCollection = new NameValueCollection();
    var key = "";
    while (reader.Read())
    {
      if (reader.TokenType == JsonToken.StartObject)
      {
        nameValueCollection = new NameValueCollection();
      }
      if (reader.TokenType == JsonToken.EndObject)
      {
        return nameValueCollection;                    
      }
      if (reader.TokenType == JsonToken.PropertyName)
      {
        key = reader.Value.ToString();
      }
      if (reader.TokenType == JsonToken.String)
      {
        nameValueCollection.Add(key, reader.Value.ToString());
      }
    }
    return nameValueCollection;
  }

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

To user the converter, add it to the converters collection as follows:

var settings = new JsonSerializerSettings()
  {
    ContractResolver = new CamelCasePropertyNamesContractResolver(),
    TypeNameHandling = TypeNameHandling.All,
    Formatting = Formatting.Indented
  };
  settings.Converters.Add(new NameValueCollectionConverter());
  JsonConvert.DefaultSettings = () => settings;
Nice job done in implementing and documenting Json.NET!

4 comments:

  1. This solution didn't work for me. I only get a list of the keys, and no values.

    ReplyDelete
    Replies
    1. Looks as if the custom converter is not called. As far as I can remember, this worked in ASP.NET MVC. If you are using ASP.NET Web API, try configuring custom converter as follows:

      public static class WebApiConfig
      {
      public static void Register(HttpConfiguration config)
      {
      // Web API configuration and services

      // Web API routes
      config.MapHttpAttributeRoutes();

      config.Routes.MapHttpRoute(
      name: "DefaultApi",
      routeTemplate: "api/{controller}/{id}",
      defaults: new { id = RouteParameter.Optional }
      );

      var jsonFormatter = config.Formatters.JsonFormatter;

      var settings = jsonFormatter.SerializerSettings;
      settings.Converters.Add(new NameValueCollectionConverter());
      }
      }

      Delete
  2. ran into an error when trying to deserialize objects with null name value collections.
    Needed a check before the while(reader.read())

    if (reader.TokenType == JsonToken.Null)
    return null;

    ReplyDelete
  3. May want to consider preserving multiple values; swapping out the writer loop:

    ```
    foreach (var key in collection.AllKeys) {
    writer.WritePropertyName(key);
    var maybeManyValues = collection.GetValues(key);
    // let the settings decide how to write null value
    if (maybeManyValues == null)
    writer.WriteValue(maybeManyValues);
    // write as a single value
    else if (maybeManyValues.Length == 1)
    writer.WriteValue(maybeManyValues[0]);
    else {
    writer.WriteStartArray();
    foreach (var val in maybeManyValues) {
    writer.WriteValue(val);
    }
    writer.WriteEndArray();
    }
    }
    ```

    ReplyDelete