JavaScript Object Notation (JSON) in .NET Part 4
May 17 2009
Welcome to Part 4 of a multi-part series on JavaScript Object Notation (JSON) support in the Microsoft .NET Framework. In this article, we'll focus on the JavaScriptConverter class from the System.Web.Script.Serialization namespace. You can find the other parts of this series at these locations:
- Part 1 - An exploration of the DataContractJsonSerializer class
- Part 2 - An exploration of the JavaScriptSerializer class
- Part 3 - JSON serialization from WCF (including a REST primer)
- Part 4 - Using the JavaScriptConverter class to customize JSON serialization
Welcome back. We've been exploring the differences in .NET's two competing JSON serialization classes for a while now. This time, let's dig into ways that we can customize the JavaScriptSerializer to solve some difficult problems. In some circumstances, the services provided by the JavaScriptSerializer for dehydrating and rehydrating of POCOs just isn't sufficient. As discussed in Part 2 of this series, the JavaScriptSerializer has the interesting capability of handling plain .NET classes that aren't marked up with serialization-related metadata in any way. Most of the competing serialization and deserialization classes in the .NET Framework Class Library require some kind of meadata, typically in the form of attributes, to assist the serializer in doing it's job. And there's good reason for this. Sometimes, simply iterating over the properties and fields of an object doesn't provide enough information to properly put it into a format that's suitable for storage or for transmission. For example, look at the following class definition which uses two properties to expose a single DateTime object in different formats:
public class Redundant
{
private DateTime _ts = DateTime.Now;
public Redundant()
{
_ts = DateTime.Now;
}
public Redundant( DateTime ts )
{
_ts = ts;
}
public string AsRFC1123
{
// return the date in RFC 1123 format
get { return _ts.ToString( "R" ); }
}
public string AsSortable
{
// return the date in a sortable format
get { return _ts.ToString( "s" ); }
}
}
When an instance of this class is serialized to JSON using the JavaScriptSerializer, it may look something like this:
{"AsRFC1123":"Sun, 17 May 2009 01:00:00 GMT","AsSortable":"2009-05-17T01:00:00"}
As you can see, the serializer decided to include both string properties, AsRFC1123 and AsSortable which is redundant. There's no need to include the DateTime information twice. However, the JavaScriptSerializer can't know what should and should not be serialized because the definition of the Redundant class has no markup to give it clues about what should be included. The JavaScriptSerializer doesn't depend on attributes as some other .NET serializers do. Instead, if you want to customize the serialization process when using the JavaScriptSerializer, you must provide an instance of a class derived from JavaScriptConverter. For the Redundant class, the following converter can be used to fix the problem:
private class RedundantConverter : JavaScriptConverter
{
public override IEnumerable<Type> SupportedTypes
{
// register the Redundant type
get { return new[] { typeof( Redundant ) }; }
}
public override IDictionary<string, object> Serialize(
object obj, JavaScriptSerializer serializer )
{
// convert the object obj into a dictionary
// of name value pairs
var r = obj as Redundant;
if (r == null) return null;
var map = new Dictionary<string, object>();
map["ts"] = r.AsSortable;
return map;
}
public override object Deserialize(
IDictionary<string, object> map,
Type type, JavaScriptSerializer serializer )
{
// read the name value pairs in the
// dictionary and rehydrate an instance
DateTime ts;
if (DateTime.TryParse( map["ts"].ToString(), out ts ))
return new Redundant( ts );
return null;
}
}
First of all, notice that there are 3 members from the abstract base class that need to be overridden. These are:
- IEnumerable<Type> SupportedTypes { get; }
- IDictionary<string, object> Serialize( object obj, JavaScriptSerializer serializer );
- object Deserialize( IDictionary<string, object> map, Type type, JavaScriptSerializer serializer );
private static void JavaScriptConverterTests()
{
var r = new Redundant();
var jser = new JavaScriptSerializer();
var rs = jser.Serialize( r );
Console.WriteLine( "Redundant serialized with" +
"out conversion support: '{0}'", rs );
jser.RegisterConverters(
new JavaScriptConverter[]
{
new RedundantConverter()
} );
rs = jser.Serialize( r );
Console.WriteLine( "Redundant serialized with " +
"conversion support: '{0}'", rs );
r = jser.Deserialize<Redundant>( rs );
Console.WriteLine("Redundant deserialized with " +
"conversion support: AsRFC1123 = '{0}', " +
"AsSortable = '{1}'", r.AsRFC1123,
r.AsSortable );
}
The output of this code will look something like this:
Redundant serialized without conversion support:
"{"AsRFC1123":"Sun, 17 May 2009 01:00:00 GMT",
"AsSortable":"2009-05-17T01:06:21"}"
Redundant serialized with conversion support:
"{"ts":"2009-05-17T01:00:00"}"
Redundant deserialized with conversion support:
AsRFC1123 = "Sun, 17 May 2009 01:00:00 GMT",
AsSortable = "2009-05-17T01:00:00"
In closing, let me make one more key observation. Serialization requires that you can get the property values from an object. Deserialization requires that you can set the property values of an object. But notice that the Redundant class has no mutators (set handlers) on its properties. However, since we're doing custom conversion using the RedundantConverter class, that doesn't matter. The converter's Deserialize method simply uses a custom constructor to inject the DateTime value rather than using properties to do it. Yet another really cool thing that the JavaScriptSerializer can do that baffles the average competing serializer class.
That's all for now. Join me next time as we look at how to consume JSON-encoded content in a Silverlight control. Enjoy!
