I've been thinking about replacing the JSON serializer in my internal codebase for some time and finally put a little effort into allowing JSON.NET from James Newton King to be plugged into my Ajax framework as a replaceable engine. My own parser has served me well, but JSON.NET is a much cleaner design and more flexible especially when dealing with extensibility. There are also a few nice features like the ability to pretty-format the generated JSON for debugging purposes which is immensely helpful when sending data to the client during development.
Although I'm keeping my original parser class (including original basic functionality) JSON.NET can now be optionally plugged in. So why not just replace the parser altogether? As it turns out I've been experimenting with different JSON serializer/deserializers and being able to keep a 'high level' wrapper object into which each of these parsers can be plugged into has been a great time saver as the interface that various application components use doesn't change one bit which is nice indeed.
Anyway, here's the basic code to provide JSON encoding and deserialization in a simple wrapper functions:
public string Serialize(object value) { Type type = value.GetType(); Newtonsoft.Json.JsonSerializer json = new Newtonsoft.Json.JsonSerializer(); json.NullValueHandling = NullValueHandling.Ignore; json.ObjectCreationHandling = Newtonsoft.Json.ObjectCreationHandling.Replace; json.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Ignore; json.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; if (type == typeof(DataRow)) json.Converters.Add(new DataRowConverter()); else if(type == typeof(DataTable)) json.Converters.Add(new DataTableConverter()); else if (type == typeof(DataSet)) json.Converters.Add(new DataSetConverter()); StringWriter sw = new StringWriter(); Newtonsoft.Json.JsonTextWriter writer = new JsonTextWriter(sw); if (this.FormatJsonOutput) writer.Formatting = Formatting.Indented; else writer.Formatting = Formatting.None; writer.QuoteChar = '"'; json.Serialize(writer, value); string output = sw.ToString(); writer.Close(); sw.Close(); return output; } public object Deserialize(string jsonText, Type valueType) { Newtonsoft.Json.JsonSerializer json = new Newtonsoft.Json.JsonSerializer(); json.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; json.ObjectCreationHandling = Newtonsoft.Json.ObjectCreationHandling.Replace; json.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Ignore; json.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; StringReader sr = new StringReader(jsonText); Newtonsoft.Json.JsonTextReader reader = new JsonTextReader(sr); object result = json.Deserialize(reader, valueType); reader.Close(); return result; }
One of the nice things about the JSON.NET parser is that there are a tons of configuration options that allow you to customize how JSON parsing occurs. You can see some of the options above for things like null value handling and managing how deep recursive loops should be handled for example.
JSON.NET also has a simplified JavaScriptConvert class that has much simpler serialization and deserialization methods, but this wrapper doesn't have access to the configuration options, so creating a custom wrapper that does exactly what I need certainly helps. As you can see above the parser has a ton of control over how many parsing aspects are handled which is really cool.
The really nice thing though - and the thing that really is missing in my parser - is extensibility. You can create custom Converters that can be plugged into the parsing pipeline to serialize and optionally deserialize custom types. Any JSON parser is likely to do well with most common types, but there may also be custom types or structures that are not supported or not well supported. For example, the ADO.NET objects, or a less obvious problem of how IDictionary types are handled (JSON.NET only supports string keys and then only those that result in valid JavaScript property/map names).
Luckily you can create your own converters, which is a common way of extensibility. The ASP.NET JavaScriptSerializer also supports extension via Converters. So, my immediate need before I could use the JSON.NET parser for a couple of old apps was to create a parser that can serialize JSON from Datatables.
Here's are a few converters that can create DataSet/DataTable and DataRow JSON with JSON.NET and turns them into simple value arrays (ie Dataset.Tables[].Rows[]). Note the converters are only for serialization not deserialization:
/// <summary> /// Converts a <see cref="DataRow"/> object to and from JSON. /// </summary> public class DataRowConverter : JsonConverter { /// <summary> /// Writes the JSON representation of the object. /// </summary> /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param> /// <param name="value">The value.</param> public override void WriteJson(JsonWriter writer, object dataRow) { DataRow row = dataRow as DataRow; // *** HACK: need to use root serializer to write the column value
// should be fixed in next ver of JSON.NET with writer.Serialize(object) JsonSerializer ser = new JsonSerializer(); writer.WriteStartObject(); foreach (DataColumn column in row.Table.Columns) { writer.WritePropertyName(column.ColumnName); ser.Serialize(writer,row[column]); } writer.WriteEndObject(); } /// <summary> /// Determines whether this instance can convert the specified value type. /// </summary> /// <param name="valueType">Type of the value.</param> /// <returns> /// <c>true</c> if this instance can convert the specified value type; otherwise, <c>false</c>. /// </returns> public override bool CanConvert(Type valueType) { return typeof(DataRow).IsAssignableFrom(valueType); } /// <summary> /// Reads the JSON representation of the object. /// </summary> /// <param name="reader">The <see cref="JsonReader"/> to read from.</param> /// <param name="objectType">Type of the object.</param> /// <returns>The object value.</returns> public override object ReadJson(JsonReader reader, Type objectType) { throw new NotImplementedException(); } } /// <summary> /// Converts a DataTable to JSON. Note no support for deserialization /// </summary> public class DataTableConverter : JsonConverter { /// <summary> /// Writes the JSON representation of the object. /// </summary> /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param> /// <param name="value">The value.</param> public override void WriteJson(JsonWriter writer, object dataTable) { DataTable table = dataTable as DataTable; DataRowConverter converter = new DataRowConverter(); writer.WriteStartObject(); writer.WritePropertyName("Rows"); writer.WriteStartArray(); foreach (DataRow row in table.Rows) { converter.WriteJson(writer, row); } writer.WriteEndArray(); writer.WriteEndObject(); } /// <summary> /// Determines whether this instance can convert the specified value type. /// </summary> /// <param name="valueType">Type of the value.</param> /// <returns> /// <c>true</c> if this instance can convert the specified value type; otherwise, <c>false</c>. /// </returns> public override bool CanConvert(Type valueType) { return typeof(DataTable).IsAssignableFrom(valueType); } /// <summary> /// Reads the JSON representation of the object. /// </summary> /// <param name="reader">The <see cref="JsonReader"/> to read from.</param> /// <param name="objectType">Type of the object.</param> /// <returns>The object value.</returns> public override object ReadJson(JsonReader reader, Type objectType) { throw new NotImplementedException(); } } /// <summary> /// Converts a <see cref="DataSet"/> object to JSON. No support for reading. /// </summary> public class DataSetConverter : JsonConverter { /// <summary> /// Writes the JSON representation of the object. /// </summary> /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param> /// <param name="value">The value.</param> public override void WriteJson(JsonWriter writer, object dataset) { DataSet dataSet = dataset as DataSet; DataTableConverter converter = new DataTableConverter(); writer.WriteStartObject(); writer.WritePropertyName("Tables"); writer.WriteStartArray(); foreach (DataTable table in dataSet.Tables) { converter.WriteJson(writer, table); } writer.WriteEndArray(); writer.WriteEndObject(); } /// <summary> /// Determines whether this instance can convert the specified value type. /// </summary> /// <param name="valueType">Type of the value.</param> /// <returns> /// <c>true</c> if this instance can convert the specified value type; otherwise, <c>false</c>. /// </returns> public override bool CanConvert(Type valueType) { return typeof(DataSet).IsAssignableFrom(valueType); } /// <summary> /// Reads the JSON representation of the object. /// </summary> /// <param name="reader">The <see cref="JsonReader"/> to read from.</param> /// <param name="objectType">Type of the object.</param> /// <returns>The object value.</returns> public override object ReadJson(JsonReader reader, Type objectType) { throw new NotImplementedException(); } }
Maybe somebody else will find this useful as well. The serialization is one way only because I personally don't have any need for two-way and deserialization of these objects is tricky because deserialization requires a type to match properties to values in the parsed JSON and well these ADO structures don't have any type format.
Anyway JSON.NET is really nice because it's highly configurable and actively developed. It's also open code so you can look through it and see how its done and tweak if necessary. One thing though - it appears it is a bit slower than other solutions. I've found JSON.NET 2.0 to be more than twice as slow as other solutions, and JSON.NET 3.0 as much as 6 to 7 times slower in some informal iteration tests built into my test suite (I noticed this because tests were slowing down drastically on the perf loop). While processing times are still very small (7 seconds for 10,000 serializations w/ JN 3.0) it still rankles a bit when its considerably slower than other solutions.
The stock JavaScriptSerializer that ships with System.Web.Extensions as part of .NET 3.5 also doesn't directly support ADO.NET objects. Some time ago there was a Converter provided in the Futures package (might still be but I haven't checked) but I never actually used it because the Futures assembly is just too much in flux and you never know what sticks around and will get axed.
Luckily it's also pretty easy to create custom converters for JavaScriptSerializer and so it's easy to create DataSet/Table/Row converters that you can add to your own apps without having to rely on the futures DLL. Here's that same implementation for the JavaScriptSerializer:
internal class WebExtensionsJavaScriptSerializer : JSONSerializerBase, IJSONSerializer { public WebExtensionsJavaScriptSerializer(JSONSerializer serializer) : base(serializer) {} public string Serialize(object value) { JavaScriptSerializer ser = new JavaScriptSerializer(); List<JavaScriptConverter> converters = new List<JavaScriptConverter>(); if (value != null) { Type type = value.GetType(); if (type == typeof(DataTable) || type == typeof(DataRow) || type == typeof(DataSet)) { converters.Add(new WebExtensionsDataRowConverter()); converters.Add(new WebExtensionsDataTableConverter()); converters.Add(new WebExtensionsDataSetConverter()); } if (converters.Count > 0) ser.RegisterConverters(converters); } return = ser.Serialize(value); } public object Deserialize(string jsonText, Type valueType) { // *** Have to use Reflection with a 'dynamic' non constant type instance JavaScriptSerializer ser = new JavaScriptSerializer(); object result = ser.GetType() .GetMethod("Deserialize") .MakeGenericMethod(valueType) .Invoke(ser, new object[1] { jsonText }); return result; } } internal class WebExtensionsDataTableConverter : JavaScriptConverter { public override IEnumerable<Type> SupportedTypes { get { return new Type[] {typeof (DataTable)}; } } public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer) { throw new NotImplementedException(); } public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer) { DataTable table = obj as DataTable; // *** result 'object' Dictionary<string, object> result = new Dictionary<string, object>(); if (table != null) { // *** We'll represent rows as an array/listType List<object> rows = new List<object>(); foreach (DataRow row in table.Rows) { rows.Add(row); // Rely on DataRowConverter to handle } result["Rows"] = rows; return result; } return new Dictionary<string, object>(); } } internal class WebExtensionsDataRowConverter : JavaScriptConverter { public override IEnumerable<Type> SupportedTypes { get { return new Type[] { typeof(DataRow) }; } } public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer) { throw new NotImplementedException(); } public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer) { DataRow dataRow = obj as DataRow; Dictionary<string, object> propValues = new Dictionary<string, object>(); if (dataRow != null) { foreach (DataColumn dc in dataRow.Table.Columns) { propValues.Add( dc.ColumnName, dataRow[dc]); } } return propValues; } } internal class WebExtensionsDataSetConverter : JavaScriptConverter { public override IEnumerable<Type> SupportedTypes { get { return new Type[] { typeof(DataSet) }; } } public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer) { throw new NotImplementedException(); } public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer) { DataSet dataSet = obj as DataSet; Dictionary<string, object> tables = new Dictionary<string, object>(); if (dataSet != null) { foreach (DataTable dt in dataSet.Tables) { tables.Add(dt.TableName, dt); } } return tables; } }
Implementing a custom converter involves creating dictionaries of property name and value pairs with the abillity to create nested properties by specifying another dictionary as a value. This approach is makes it pretty easy to create custom serialization formats...
I know I'm not using DataSets much anymore these days but with older code still sitting around that does these converters are coming in handy. Maybe this will prove useful to some of you in a similar situation.