-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Extending Serialization with Converters
The default serialization provided by Nancy for JSON data should in most instances automatically do the right thing. Reflection is used to obtain a list of data members in the object, and then each data member's value is converted to the correct type and assigned. If the data member's type is itself a class type, then the process is used, recursively, for that object as well.
Sometimes, though, there might be circumstances when you need more than this simple algorithm to translate data.
Consider, for instance, if your external interface had a requirement that a date needed to be specified as its individual components, year, month and day:
{"orderDate":{"year":2014,"month":7,"day":12}}
You could just make a data type that stored year
, month
and day
fields, but then every time you needed to treat the order date as a DateTime
, you'd have to convert the values manually. The serialization system in Nancy allows you to do better than this: Using a specialized JavaScriptConverter
, you can intercept the serialization and deserialization of DateTime
values and emit the data in any format you want. The way a JavaScriptConverter
works saves you from having to directly manipulate JSON data; rather than JSON text, the serialized form is an associative array, an IDictionary<string, object>
. When deserializing, the JSON text is converted to this form before any other conversion is done. When serializing, this dictionary is then translated by Nancy into the text representation of a JSON object, and whatever objects you place into the array go back into the start of the serializer and can themselves be handled by converters.
To create a JavaScriptConverter
, you need to do two things:
- You need to indicate to the serialization system what data types your converter will be handling.
- You need to provide the code that translates between your data type and
IDictionary<string, object>
.
These steps are done by implementing the abstract members of the JavaScriptConverter
base class.
To indicate which data types your converter supports, you need to supply an implementation of the SupportedTypes
property. This property returns an IEnumerable<Type>
, which means a simple implementation can look like this:
public override IEnumerable<Type> SupportedTypes
{
get { yield return typeof(DateTime); }
}
A more complicated converter supporting multiple types could store the list in a static array and return that array, since arrays implement IEnumerable`1
as well.
To perform the actual conversion, supply implementations of Serialize
and Deserialize
:
public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
{
if (obj is DateTime)
{
DateTime date = (DateTime)obj;
var json = new Dictionary<string, object>();
json["year"] = date.Year;
json["month"] = date.Month;
json["day"] = date.Day;
return json;
}
return null;
}
public override object Deserialize(IDictionary<string, object> json, Type type, JavaScriptSerializer serializer)
{
if (type == typeof(DateTime))
{
object year, month, day;
json.TryGetValue("year", out year);
json.TryGetValue("month", out month);
json.TryGetvalue("day", out day);
if ((year is int)
&& (month is int)
&& (day is int))
return new DateTime((int)year, (int)month, (int)day);
}
return null;
}
You can choose to do as much or as little conversion as you want. This sample, for instance, is pretty strict about the fields it expects to see; they must be int
values. It is not strict about whether there are other fields present, though. A stricter implementation might refuse to translate a JSON object with extra properties, while a less strict implementation might accept and attempt to convert non-integer values. JavaScriptConverter
allows you to tailor the serialization and deserialization processes to your exact needs.
There are some instances where you might want to control the conversion of an object directly to/from a primitive JSON value, rather than a JSON object. For instance, if your models include byte arrays, you might want to serialize these as Base 64 strings. The default serializer will read and write JSON text like [72,101,108,108,111,44,32,119,111,114,108,100]
. The corresponding Base 64 representation of this is SGVsbG8sIHdvcmxk
, which is significantly shorter and also faster to parse. You could simply declare the data field as a string
in your model, but it is possible to make this conversion take place seamlessly during the serialization and deserialization processes using a JavaScriptPrimitiveConverter
.
A JavaScriptPrimitiveConverter
is quite similar to a JavaScriptConverter
, except that the JSON representation is simply an object
, rather than a full IDictionary<string, object>
. Whatever value you return in that object
will be written directly to JSON output during serialization. When deserializing, if the data type for the corresponding field in your model type is matched by a JavaScriptPrimitiveConverter
, then the primitive value is handed over to the converter, which may return any C# object it desires.
The steps for implementing a JavaScriptPrimitiveConverter
are the same as those for creating a JavaScriptConverter
:
- You need to indicate to the serialization system what data types your converter will be handling.
- You need to provide the code that translates between your data type and
object
.
To indicate which data types your converter supports, supply an implementation of the SupportedTypes
property. See the description above of the JavaScriptConverter
property by the same name for more information.
To perform the actual conversion, supply implementations of Serialize
and Deserialize
:
public override IEnumerable<Type> SupportedTypes
{
get { yield return typeof(byte[]); }
}
public override object Serialize(object obj, JavaScriptSerializer serializer)
{
var byteArray = obj as byte[];
if (byteArray != null)
{
try
{
return Convert.ToBase64String(byteArray);
}
catch { }
}
return null;
}
public override object Deserialize(object jsonPrimitive, Type type, JavaScriptSerializer serializer)
{
if ((type == typeof(byte[])) && (jsonPrimitive is string))
{
try
{
return Convert.FromBase64String(jsonPrimitive as string);
}
catch { }
}
return null;
}
Another possible use of a JavaScriptPrimitiveConverter
might be to support XmlElement
fields embedded in model types. By default, XmlElement
instances won't produce any output with the JSON serializer, because the data type does not have any data members that can be both read and written to. However, a JavaScriptPrimitiveConverter
could take over handling of XmlElement
values and convert the subtrees they represent into the corresponding XML text, to be output as a JSON string for transport.
JavaScriptConverter
s and JavaScriptPrimitiveConverter
s must be enabled in each JavaScriptSerializer
object in order to be active. There are two ways to accomplish this:
-
If you are creating a
JavaScriptSerializer
for a specific task within your code, you can explicitly register converters using theRegisterConverters
method. This method allows a series ofJavaScriptConverter
and/orJavaScriptPrimitiveConverter
objects to be supplied. -
If you need to use the converters in a situation where the actual
JavaScriptSerializer
object is being implicitly created, or if you need the effects of a converter to be available globally, you can add converters to the static collectionsJsonSettings.Converters
andJsonSettings.PrimitiveConverters
. TheJavaScriptSerializer
objects used within model binding and default model serialization by Nancy automatically register any converters found in these collections at the time they are constructed.
Nancy uses the .NET Framework's own built-in XmlSerializer
infrastructure to handle clients sending and receiving data using XML as the transport format. The design of XmlSerializer
is quite extensible, and the way in which it is extensible is different from the JavaScriptConverter
and JavaScriptPrimitiveConverter
types that JSON serialization employs. XmlSerializer
is unaware of JSON converters, and JavaScriptSerializer
ignores XML-specific attributes. Thus, extensions to XML serialization and to JSON serialization can coexist in the same project without interfering with one another.
It is beyond the scope of this documentation to fully explain how to take control of XmlSerializer
's serialization & deserialization processes, but the effects in the sample converters above can be handled with XML in two ways:
-
The
[XmlIgnore]
attribute can be supplied to ensure that a field whose data type cannot be handled by the default serialization is skipped. The data within that field can then be formatted in any way you want by the use of a second property. This property can be made invisible in the IDE using the[EditorBrowsable]
attribute, and the exact XML element or attribute name to be used can be supplied using an[XmlElement]
or[XmlAttribute]
attribute. Note that the second property must fully support reading and writing to work property withXmlSerializer
. -
For more complicated scenarios, there is an interface that
XmlSerializer
checks for. Data types that implementIXmlSerializable
can take complete charge over the translation of XML data to/from C# objects.
More information can be found at MSDN, as well as in many tutorials available on the web.
- Introduction
- Exploring the Nancy module
- Routing
- Taking a look at the DynamicDictionary
- Async
- View Engines
- Using Models
- Managing static content
- Authentication
- Lifecycle of a Nancy Application
- Bootstrapper
- Adding a custom FavIcon
- Diagnostics
- Generating a custom error page
- Localization
- SSL Behind Proxy
- Testing your application
- The cryptography helpers
- Validation
- Hosting Nancy with ASP.NET
- Hosting Nancy with WCF
- Hosting Nancy with Azure
- Hosting Nancy with Suave.IO
- Hosting Nancy with OWIN
- Hosting Nancy with Umbraco
- Hosting Nancy with Nginx on Ubuntu
- Hosting Nancy with FastCgi
- Self Hosting Nancy
- Implementing a Host
- Accessing the client certificate when using SSL
- Running Nancy on your Raspberry Pi
- Running Nancy with ASP.NET Core 3.1