Click here to Skip to main content
15,861,172 members
Articles / Programming Languages / C#
Alternative
Article

PowerJSON - A Powerful and Fast JSON Serializer

Rate me:
Please Sign up or sign in to vote.
5.00/5 (30 votes)
2 Feb 2018CPOL18 min read 72.9K   870   63   33
This is a fork of "fastJSON" with new power to control many aspects in JSON serialization and deserialization, such as, serializing interface instances (polymorphic serialization) and private types, including or excluding members, performing data conversions, conditional serialization, etc.
NOTICE:

Months ago, I ran a performance test against several JSON serializers and found that:

  1. fastJSON was actually not so fast at all.
  2. Newtonsoft's JSON serializer had developed somewhat and its performance was enhanced, quite closed to fastJSON
  3. The fastest JSON serializer was NetJSON which used dynamic assemblies to boost the performance to the edge. The architecture of fastJSON would never lead it to be a counterpart to NetJSON.

Hereby, I had deprecated the synchronization with fastJSON and the development of PowerJSON.

The article was left here for its historical reasons. I recommend NetJSON as it is the fastest JSON serializer today.

Feb 3rd, 2018

Contents

  1. Introduction
  2. Background
  3. Basic Usage
  4. Transforming Data Before Serialization and Deserialization
  5. Serializing Unsupported Types with JsonConverter
  6. Conditionally Intercepting Serialization and Deserialization
  7. What about Performance
  8. History

Introduction

This is a fork, an enhanced version, of 's brilliant project fastJSON. It adds some new classes, interfaces and custom attributes to facilitate JSON serialization and deserialization of objects. It also fixes some issues in the original version.

Although many features have been added to the original version, the performance is not sacrificed but even improved after thorough optimizations and refactoring.

The fork can be accessed on CodePlex at https://github.com/wmjordan/PowerJSON.

Background

If fastJSON is new to you, it is recommended that you read this article before you begin.

PowerJSON is forked from fastJSON and is almost fully compatible with it. Therefore, the assembly file name of PowerJSON is fastJSON.dll and the type library uses <span class="literal">fastJSON</span> as the namespace.

Besides the long features list of the original fastJSON, PowerJSON provides the following advanced features:

  1. Serialize member names in camel-case or uppercase, without forcing your data models to break the .NET coding convention merely to fit JSON protocols.
  2. Assign another name for the member in serialization.
  3. Polymorphic serialization without turning on the UseExtensions setting: Serialize and deserialize interface instances rather than concrete class types; serialize and deserialize abstract classes.
  4. Assign another literal name for an Enum value.
  5. Include a specific read-only member during serialization, or exclude a specific public member from being serialized.
  6. Serialize and deserialize private/internal classes, structs, fields or properties.
  7. Transform data before serialization (for instance, encrypt the serialized data, serialize arrays into comma separated strings, etc.) and vice versa before deserialization.
  8. Conditionally serialize or deserialize members.
  9. Write out additional data in serialization.
  10. Transform or validate data before (or after) serialization or deserialization.
  11. Invasiveless control of serialization and deserialization--altering the serialization result without changing the data model, which is especially helpful when you don't have the source code for them.
  12. Serializing HashSet<T> and other enumerable types which have an Add(?) method.
  13. Easiest customized serialization and deserialization. You can implement serialization for unsupported types with the least effort.
  14. A documentation file for the library.

This fork also fixes some issues in the original fastJSON, such as TimeSpan instances could cause stack overflow, multi-value items in NameValueCollection could not be deserialized as before serialization, etc. Please refer to the readme.md file in the source code for more details.

Basic Usage

The following section will describe how to use PowerJSON to control the serialization result.

A Sample Class

One of the most common issues of JSON serialization is the naming of JSON structures and our .NET types. Given that we have a class like the following:

C#
internal class DemoClass
{
    public string MyProperty { get; set; }

    public MyEnum MyEnumProperty { get; set; }

    public int Number { get; set; }

    public object Identifier { get; set; }

    public int InternalValue { get; set; }

    private int privateField;
}
C#
public enum MyEnum
{
    None,
    Vip
}
public class ClassA
{
    public string Name { get; set; }
}
public class ClassB
{
    public int Code { get; set; }
}

And we want to fulfill the following requirements:

  • DemoClass is an internal class, which, by default, is NOT serializable for its constructor is not publicly visible, but we want to make it deserializable.
  • All properties should have camel-case serialized names.
  • MyProperty is serialized to the name "prop".
  • MyEnumProperty is serialized to have a name "enum".
  • Number should not be serialized if its value is 0.
  • Identifier can be of type ClassA or ClassB, and have a serialized name as "a" or "b", respectively. If Identifier is not of the above two types, its serialized name should be "variant".
  • InternalValue should not be serialized or deserialized.
  • privateField should be serialized.
  • The name of the Vip field in MyEnum type should be serialized as "VIP".

The above requirements could not be fulfilled with fastJSON, but with PowerJSON, it is possible, and there are two ways.

  1. Invasive Mode: Add Custom Attributes to the data model, i.e., DemoClass here, to control the serialization.
  2. Noninvasive Mode: Leave the data model along. Programmatically control the serialization result.

Invasive Mode: Controlling Serialization with Custom Attributes

The invasive mode of serialization control adds custom attributes to the classes or structs we want to take control.

Since this approach touches the source code, it is called Invasive Mode.

The following code shows how to use custom attributes to control the serialization result.

C#
// marks the internal DemoClass class deserializable
[JsonSerializable]
internal class DemoClass
{
    // marks MyProperty property to be serialized to a field named "prop"
    [JsonField ("prop")]
    public string MyProperty { get; set; }

    // marks MyEnumProperty property to be serialized to a field named "enum"
    [JsonField ("enum")]
    public MyEnum MyEnumProperty { get; set; }

    // marks not to serialize the Number property, if its value is 0
    [JsonNonSerializedValue (0)]
    public int Number { get; set; }

    // marks the serialized name of Identifier will be "a", if its type is ClassA,
    //     and "b" for ClassB, and "variant" for other types
    [JsonField ("a", typeof (ClassA))]
    [JsonField ("b", typeof (ClassB))]
    [JsonField ("variant")]
    public object Identifier { get; set; }

    // marks the InternalValue property will not be serialized
    [JsonInclude (false)]
    // marks the InternalValue property will not be deserialized
    [System.ComponentModel.ReadOnly (true)]
    public int InternalValue { get; set; }

    // marks the privateField serializable
    [JsonSerializable]
    private int privateField;
}

public enum MyEnum
{
    None,
    // marks the serialized name of Vip to "VIP"
    [JsonEnumValue ("VIP")]
    Vip
}

After annotating the data model with custom attributes, we can serialize the data model.

Firstly, setup the default serialization settings to fulfill this requirement--all properties should have camel-case serialized names.

C#
JSON.Parameters.NamingConvention = NamingConvention.CamelCase;
JSON.Parameters.UseExtensions = false;

Secondly, initialize the data model with some values.

JavaScript
var d = new Demo1.DemoClass () {
    MyProperty = "p",
    Number = 1,
    MyEnumProperty = Demo1.MyEnum.Vip,
    InternalValue = 2,
    Identifier = new ClassA () { Name = "c" }
};

Finally, call the ToJSON method to get the serialization result.

JavaScript
var s = JSON.ToJSON (d);
Console.WriteLine (s);

The above example gives the following result.

JavaScript
{"prop":"p","enum":"VIP","number":1,"a":{"name":"c"},"privateField":0}

To deserialize the JSON string back to an instance of DemoClass, use the ToObject<span id="LST5A47910_0"><T<span id="LST5A47910_1">></span></span> method.

C#
var o = JSON.ToObject<Demo1.DemoClass> (s);

Noninvasive mode: Controlling Serialization with SerializationManager

The invasive mode is quite simple--simply marking the classes and members with custom attributes and things are done. However, there are some disadvantages of custom attributes. Some of them are listed below:

  1. Issue 1: Custom attributes require modifications on source code, but sometimes it is impossible, e.g., we can't modify the CLR types in the .NET Framework.
  2. Issue 2: They invade the data models and make them rely on the PowerJSON library.
  3. Issue 3: They may conflict, typically when the same data model is required to be serialized to various forms.

To surmount the above issues, PowerJSON has introduced a Noninvasive Mode of serialization control. The noninvasive mode makes no modification on the data models, yet provides no less power than the invasive mode.

The noninvasive mode is majorly implemented with the following classes: SerializationManager, TypeOverride and MemberOverride.

The default instance of SerializationManager can be access from the static Manager property in the JSON class. Calling its Override<span id="LST5A47910_4"><T<span id="LST5A47910_5">></span></span> method, which takes an instance of TypeOverride, will tell the serialization engine how to alter the result of serialization or deserialization.

The following code gives a usage example of using SerializationManager.

C#
// overrides the serialization behavior of DemoClass
JSON.Manager.Override<DemoClass> (new TypeOverride () {
    // makes DemoClass always deserializable
    Deserializable = true,
    // override members of the class
    MemberOverrides = {
        // assigns the serialized name "prop" to MyProperty property
        new MemberOverride ("MyProperty", "prop"),
        new MemberOverride ("MyEnumProperty", "enum"),
        // assigns a default value to the Number property
        new MemberOverride ("Number") { NonSerializedValues = { 0 } },
        // assigns default serialized name and typed serialized name
        new MemberOverride ("Identifier", "variant") {
            TypedNames = {
                { typeof(ClassA), "a" },
                { typeof(ClassB), "b" }
            }
        },
        // denotes the InternalValue property is neither serialized nor deserialized
        new MemberOverride ("InternalValue") {
            Deserializable = false,
            Serializable = false
        },
        new MemberOverride ("privateField") {
            Serializable = true,
            Deserializable = true
        }
    }
});

// changes the serialized name of the "Vip" field of the MyEnum enum type
JSON.Manager.OverrideEnumValueNames<MyEnum> (new Dictionary<string, string> {
    { "Vip", "VIP" }
});

To serialize or deserialize the data model, use the same function call as the invasive mode to the JSON class. And the output will be the same as the invasive mode example.

With the SerializationManager, serialization can be controlled from external code. Issue 1 and Issue 2 is resolved. To solve Issue 3, conflictive serializations, we will use an alternating SerializationManager, which is discussed below.

Alternating the SerializationManager

To demonstrate why custom attributes may conflict and how to resolve the conflict with the alternation of SerializationManager, let's take a look at the following classes first.

C#
public class Group
{
    public int ID { get; set; }
    public string Name { get; set; }
    public List<Member> Members { get; private set; } = new List<Member>();
}
public class Member
{
    public int GroupID { get; set; }
    public string Name { get; set; }
}

The above code snippet has two classes. A Group class has a list containing multiple instances of Member class.

A normal serialization of the Group class may look like this (whitespaces added for clarity):

C#
{"ID": 1,
"Name": "group name",
"Members": [
    { "GroupID": 1, "Name": "a" },
    { "GroupID": 1, "Name": "b" },
    { "GroupID": 1, "Name": "c" }
]}

We can see that the "GroupID":1 is repeated multiple times in each serialized Member instance. Since all members belong to the group 1, which is already represented in "ID": 1 and implied by the cascading structure of the JSON result, those "GroupID": 1 fields could be omitted for conciseness.

So we may hope that:

  1. The GroupID fields should be hidden when they are serialized within a Group instance to generate a compact, unredundant result.
  2. The GroupID fields should be visible when they are individually serialized within the Member instance.
JavaScript
// the desired serialization result of a Group instance
{"ID": 1,
"Name": "group name",
"Members": [
    { "Name": "a" },
    { "Name": "b" },
    { "Name": "c" }
]}
// the desired serialization result of a Member instance
{"GroupID":1, "Name":"a"}

Adding serialization control onto the GroupID member of the Member class can hide the GroupID field, however this also hides it from the individual serialization result of the Member class, generating a result like this, which is undesirable.

JavaScript
{"Name":"a"}

To fulfill both serialization requirements of the Group and Member classes, we shall use an alternating SerializationManager and pass it into the ToJSON(Object, JSONParameters, SerializationManager) method overload which takes a SerializationManager as a parameter.

The alternating SerializationManager passed to the ToJSON method can override the Group class to hide the GroupID field, and the default one can be used to serialize the Member class, which reveals the GroupID field by default, like the following code shows.

C#
var g = new Group () {
    ID = 1,
    Name = "test",
    Members = {
        new Member () { GroupID = 1, Name = "a" },
        new Member () { GroupID = 1, Name = "b" },
        new Member () { GroupID = 1, Name = "c" }
    }
};

var gsm = new SerializationManager ();
gsm.Override<Member> (new TypeOverride () {
    MemberOverrides = { new MemberOverride ("GroupID", false) }
});

// use the alternated SerializationManager
var s1 = JSON.ToJSON (g, JSON.Parameters, gsm);
Console.WriteLine ("Group: " + s1);
Assert.IsFalse (s1.Contains ("GroupID")); // "GroupID" is invisible

 // use the default SerializationManager
s1 = JSON.ToJSON (g.Members[0]);
Console.WriteLine ("Member: " + s1);
StringAssert.Contains (s1, "GroupID"); // "GroupID" is visible

The above code outputs the following result, which is what we need:

JavaScript
Group: {"ID":1,"Name":"test","Members":[{"Name":"a"},{"Name":"b"},{"Name":"c"}]}
Member: {"GroupID":1,"Name":"a"}

Transforming Data Before Serialization and Deserialization

During cross-platform communication, we sometimes may want to transform our data to other forms to suite the need on other platforms. For instance,

  1. Encrypting critical information in the JSON.
  2. Converting Arrays to comma separated values.
  3. Different presentation of DateTime values.
  4. Serialize Enum values to numbers.

Ordinary serializers may require you to create a data model or add extra fields and serialize them. With PowerJSON, no modification on the data model is required. This keeps the original model clean and fits the requirements at the same time. This is done with the feature of JSON Converter.

We shall firstly write a class to implement the IJsonConverter interface which will be used before serialization and deserialization.

Before serialization, the SerializationConvert method of that interface will be called, with the name and the value of the object being serialized. The corresponding parameters are stored in an instance of JsonItem. Before JSON deserialization, DeserializationConvert will be called, and the JsonItem will be passed into the method as well.

The converter can change the Value of the JsonItem instance in the above methods, thus the serialized value can be a different type from the original data model.

Data Transformation, Encryption

The following code demonstrates how IJsonConverter works. During serialization, the string to be encrypted (item.Value) will be passed into SerializationConvert. And this class fakes an "encryption" by prefixing "Encrypted: " before the string. In deserialization, the encrypted string will be passed into DeserializationConvert and the method fakes a "decryption" by removing the prefix.

C#
class FakeEncryptionConverter : IJsonConverter
{
    public Type GetReversiveType (JsonItem item) {
        // no type conversion is required
        return null;
    }
    public void SerializationConvert (JsonItem item) {
        var s = item.Value as string;
        if (s != null) {
            item.Value = "Encrypted: " + s; // returns an encrypted string
        }
    }
    public void DeserializationConvert (JsonItem item) {
        var s = item.Value as string;
        if (s != null && s.StartsWith ("Encrypted: ")) {
            item.Value = s.Substring ("Encrypted: ".Length);
        }
    }
}

When the converter is ready, we can apply the converter onto any string fields we want to "encrypt". Before serialization, the FakeEncryptionConverter will "encrypt" MyProperty to produce the serialization result, and "decrypt" it before deserialization. The class DataConverterTestSample is completely innocent of all this behind, and simply does its job as if MyProperty is never been "encrypted" or "decrypted".

C#
public class DataConverterTestSample
{
    [JsonConverter (typeof(FakeEncryptionConverter))]
    public string MyProperty { get; set; }
}

Data Type Conversion and Advanced Polymorphic Serialization

We can implement the IJsonConverter to convert different data types between serialization and deserialization. For example, the following definition uses Int32ArrayConverter to convert int arrays into strings before serialization, and vice versa before deserialization.

C#
public class CustomConverterType
{
    [JsonConverter (typeof (Int32ArrayConverter))]
    [JsonField ("arr")]
    public int[] Array { get; set; }

    public int[] NormalArray { get; set; }

    [JsonConverter (typeof (Int32ArrayConverter))]
    [JsonField ("intArray1", typeof (int[]))]
    [JsonField ("listInt1", typeof (List<int>))]
    public IList<int> Variable1 { get; set; }

    [JsonConverter (typeof (Int32ArrayConverter))]
    [JsonField ("intArray2", typeof (int[]))]
    [JsonField ("listInt2", typeof (List<int>))]
    public IList<int> Variable2 { get; set; }
}


class Int32ArrayConverter : IJsonConverter
{
    public Type GetReversiveType (JsonItem item) {
        return null;
    }
    public void DeserializationConvert (JsonItem item) {
        var s = item.Value as string;
        if (s != null) {
            item.Value = Array.ConvertAll (s.Split (','), Int32.Parse);
        }
    }
    public void SerializationConvert (JsonItem item) {
        var l = item.Value as int[];
        if (l != null) {
            item.Value = String.Join (",", Array.ConvertAll (l, Convert.ToString));
        }
    }
}

Sample input of the above code is:

C#
var c = new CustomConverterType () {
    Array = new int[] { 1, 2, 3 },
    NormalArray = new int[] { 2, 3, 4 },
    Variable1 = new int[] { 3, 4 },
    Variable2 = new List<int> { 5, 6 }
};

And the output is:

JavaScript
{"arr":"1,2,3","NormalArray":[2,3,4],
"intArray1":"3,4","listInt2":[5,6]}

Please note: During deserialization, the value of JsonItem is not fully deserialized and converted to the type of the property yet. The value before fully deserialized has one of the primitive deserialization types. There are six possible primitive deserialization types: Boolean for boolean values, Int64 for integers, Double for fractional numbers (decimal type is also converted into Double), String for literal values, enum, DateTime and TimeSpan types, IList<Object> for arrays, and IDictionary<string, object> for general class or structs, and of course, null values are also possible. The Value of the JsonItem passed into DeserializationConvert method could be one of the above six types.

Nonetheless, you don't have to care about primitive deserialization types during serialization. The serializer can handle the conversion.

To avoid handling primitive deserialization types, the implementation of the IJsonConverter should write a GetReversiveType method, which tells the deserialization engine the expected type of the DeserializationConvert method, and the deserializer will try to convert the primitive value to the type returned from the GetReversiveType method.

For example, you are trying to serialize a DateTime property adding one hour for the daylight-saving time and subtract one hour while deserializing it. Without giving the type from the GetReversiveType method, the code will look like this:

C#
class DayLightSavingTimeConverter : IJsonConverter {
    public Type GetReversiveType (JsonItem item) {
        return null;
    }
    public void SerializationConvert (JsonItem item) {
       if (item.Value is DateTime) {
           // converts the DateTime value to an hour later
           item.Value = ((DateTime)item.Value).AddHours(1);
       }
    }
    public void DeserializationConvert (JsonItem item) {
        // the primitive deserialization type of DateTime is string
        DateTime d = DateTime.Parse((String)item.Value, System.Globalization.CultureInfo.InvariantCulture);
        item.Value = d.AddHours(-1);
    }
}

To simplify the code, we have the GetReversiveType method return the typeof(DateTime), to tell the deserializer the expected data type is DateTime, like this:

C#
class DayLightSavingTimeConverter : IJsonConverter {
    public Type GetReversiveType (JsonItem item) {
       // tells the deserialization engine to convert the Value
       // in JsonItem to DateTime type
        return typeof(DateTime);
    }
    public void SerializationConvert (JsonItem item) {
       if (item.Value is DateTime) {
           // converts the DateTime value to an hour later
           item.Value = ((DateTime)item.Value).AddHours(1);
       }
    }
    public void DeserializationConvert (JsonItem item) {
       // the primitive deserialization type of DateTime is string
       // but it is converted to DateTime by the effect of
       // GetReversiveType method
        DateTime d = (DateTime)item.Value;
        item.Value = d.AddHours(-1);
    }
}

Type conversion with JsonConverter<>

The primitive deserialization types may have you feel a bit cumbersome to write the DeserializationConvert part. Fortunately, if you have the converter class inherit from the JsonConverter<> class, the deserializer will internally detect the possible type (the converted type, rather than the original type) during deserialization, and try its best to convert the primitive deserialization type to suite the need of the DeserializationConvert method.

Let's see how it works by comparing to the above code.

C#
class DateConverter : JsonConverter<DateTime, DateTime>
{
    protected override DateTime Convert (string fieldName, DateTime fieldValue) {
        return fieldValue.AddHours (1);
    }
 
    protected override DateTime Revert (string fieldName, DateTime fieldValue) {
        return fieldValue.AddHours (-1);
    }
}

The DateConverter is based on a generic helper class JsonConverter<>, which implements the IJsonConverter interface, converting between two specific member types during conversion.

The JsonConverter<> class has two protected abstract methods: Convert and Revert. Convert is called before serialization. It takes the name and value of the member being serialized. The return value will be serialized to the JSON output. Revert is called before deserialization and the return value will be used as the result of deserialization, or set to the field or property of the deserialized object.

When implementing those two methods, you can directly convert between .NET data types rather than dealing with primitive JSON types.

Numeric Enum Value Serialization

With the JsonConverter<>, it is also possible to specifically serialize a certain enum types into numbers without turning on the UseValuesOfEnums setting in JSONParameters, which affects all enum types.

The following code demonstrates how it works. The class below has two properties of enum types. Assuming that we want the first one to be serialized as usual--the value is serialized into the literal form, but the second one is serialized into the numeric form. We can use the JsonConverterAttribute to mark the second property. And then we implement the NumericEnumConverter, inheriting from the JsonConverter<> class.

C#
public class EnumTestSample
{
    public Fruits MyFruit { get; set; }
    [JsonConverter (typeof (NumericEnumConverter))]
    public Fruits NumericFruit { get; set; }
}

public class NumericEnumConverter : JsonConverter<Fruits, int>
{
    protected override int Convert (string fieldName, Fruits fieldValue) {
        return (int)fieldValue;
    }

    protected override Fruits Revert (string fieldName, int fieldValue) {
        return (Fruits)fieldValue;
    }
}

With the above code ready, the second property NumericFruit will be serialized into a numeric form.

Complex Type Conversion

We can do more complex type conversion, like the following code shows. The code will have a string-typed property to be serialized into a JSON structure defined in a class, PersonInfo.

C#
public class PersonInfo
{
    public string Name { get; set; }
    public bool Vip { get; set; }
}
public class CustomConverterType
{
    [JsonConverter (typeof (PersonInfoConverter))]
    public string Master { get; set; }
    [JsonConverter (typeof (PersonInfoConverter))]
    public string Worker { get; set; }
}
class PersonInfoConverter : JsonConverter<string, PersonInfo>
{
    protected override PersonInfo Convert (string fieldName, string fieldValue) {
        return new PersonInfo () {
            Name = fieldValue.EndsWith ("*") ? 
                   fieldValue.Substring (0, fieldValue.Length - 1) : fieldValue,
            Vip = fieldValue.EndsWith ("*")
        };
    }

    protected override string Revert (string fieldName, PersonInfo fieldValue) {
        // no need to deal with the primitive deserialization type (Dictionary<string,object>) any more
        return fieldValue.Name + (fieldValue.Vip ? "*" : null);
    }
}

With the above definition, we create an instance like the following code:

C#
var d = new CustomConverterType() { Master = "WMJ*", Worker = "Gates" };

The serialized result can look like this:

{"Master":{"Name":"WMJ","Vip":true},
"Worker":{"Name":"Gates","Vip":false}}

Without the help of the JsonConverterAttribute, the serialization result would be this:

C#
{"Master":"WMJ*","Worker":"Gates"}

Serializing Unsupported Types with JsonConverter

Although PowerJSON has added supports for serializing more types. It could not yet cover all data types in the practical world. Various JSON serializers has provided mechanisms to support customized serialization. fastJSON provided Serialize and Deserialize delegates, JSON.net has also provided an abstract JsonConverter class for this purpose (see this API documentation). However, in all those mechanisms, you have to maintain the format of the JSON when implementing customized serialization, and deal with primitive types in deserialization. With the JsonConverter in PowerJSON, you don't need to care about those issues. Just focus on the data itself is enough, the rest will be handled by the serialization engine.

The Simplicity of Customized Serialization

Firstly let's take a look at the source code of JSON.net and see how it implements a custom JsonConverter which serialize and deserialize the Version class.

C#
public class VersionConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null) {
            writer.WriteNull();
        }
        else if (value is Version) {
            writer.WriteValue(value.ToString());
        }
        else {
            throw new JsonSerializationException("Expected Version object value");
        }
    }

    public override object ReadJson(JsonReader reader, 
        Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null) {
            return null;
        }
        else {
            if (reader.TokenType == JsonToken.String) {
                try {
                    Version v = new Version((string)reader.Value);
                    return v;
                }
                catch (Exception ex) {
                    throw JsonSerializationException.Create(reader, 
                    "Error parsing version string: {0}".FormatWith
                    (CultureInfo.InvariantCulture, reader.Value), ex);
                }
            }
            else {
                throw JsonSerializationException.Create(reader, 
                       "Unexpected token or value when parsing version. 
                       Token: {0}, Value: {1}".FormatWith(CultureInfo.InvariantCulture, 
                       reader.TokenType, reader.Value));
            }
        }
    }

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

The above code is very long and you see that it requires you to learn about JsonReader, JsonWriter, JsonSerializer, JsonToken types. With PowerJSON, the conversion support for Version class, although was not supported internally, can be implemented easily without any knowledge of the implementation of the JSON serialization engine.

C#
public class VersionConverter : JsonConverter<Version, string>
{
    protected override string Convert (string fieldName, Version fieldValue) {
        return fieldValue != null ? fieldValue.ToString () : null;
    }

    protected override Version Revert (string fieldName, string fieldValue) {
        try {
            return fieldValue != null ? new Version (fieldValue) : null;
        }
        catch (Exception) {
            throw new JsonSerializationException ("Error parsing version string: " + fieldValue);
        }
    }
}

You can see that you don't need to think about WriteNull, WriteValue, TokenType, (string)reader.Value, etc. The only thing needed is the code to convert between the Version and string types. Once the conversion code is written, it is done. JsonConverter will automatically convert the object and JSON string back and forth.

To apply the converter, add one line before the first serialization and deserialization begins and the VersionConverter will be applied to all instances of the Version class.

C#
JSON.Manager.OverrideConverter<Version> (new VersionConverter ());

Run the following code and the output will be "1.2.3.1234" (with quotation marks).

C#
var v = new Version (1, 2, 3, 1234);
var s = JSON.ToJSON (v);
Console.WriteLine (s);

More Complex Case of Customized Serialization

The above case is quite simple since the type is serialized to a string. The next example is a little more complex, which serializes Regex.

The Regex class could not be normally serialized or deserialized, since it does not expose a property for the pattern, which is essential to create the instance, nor does it provide a parameterless constructor, which is required by the deserializer.

The serialization implementation of Regex in JSON.net is too long to be listed here. You can click this link to read those 100+ lines code. For PowerJSON, this case is still simple.

To serialize the Regex class, we can create a model class RegexInfo which contains the pattern and options fields, and convert the Regex class to that model. Since the model has default constructor and all needed fields, it can be handled by the JSON serializer and deserializer. Here's the implemetation for the Regex converter.

C#
class RegexConverter : JsonConverter<Regex, RegexConverter.RegexInfo>
{
    protected override RegexInfo Convert (string fieldName, Regex fieldValue) {
        return new RegexInfo () { Pattern = fieldValue.ToString (), Options = fieldValue.Options };
    }
    protected override Regex Revert (string fieldName, RegexInfo fieldValue) {
        return new Regex (fieldValue.Pattern, fieldValue.Options);
    }

    [JsonSerializable]
    internal struct RegexInfo
    {
        public string Pattern;
        public RegexOptions Options;
    }
}

The above 16 lines (including blank lines) make the Regex JSON serializer about ready to be used. The last thing to do is assigning the converter to Regex as the following line shows. Afterwards, all Regex instances can be serialized and deserialized. The implementation is much more natural and simplier than JSON.net or the original fastJSON, which requires you to write raw JSON strings and read them back.

C#
JSON.Manager.OverrideConverter<Regex> (new RegexConverter ());
Tips:

fastJSON.BonusPack.Converters in PowerJSON has various converters to be used.

Conditionally Intercepting Serialization and Deserialization

PowerJSON enables you to conditionally serialize or deserialize specific members according to external situations at run time. It is can be done with the IJsonInterceptor interface and JsonInterceptor<T> class, along with the JsonInterceptorAttribute.

The serialization of an object has three phases which can be interception points:

  1. The serialization engine gets the object to be serialized.
  2. The serialization engine looks into the structure of the object, and loop through each field or property to serialize them.
  3. The serialization engine finishes the serialization.

The interceptor will allow you to take control in the above three phases, to determine the following five issues:

  1. Should a specific object be serialized.
  2. Should a member in the object be serialized.
  3. Should the name or value of the serialized member be changed before it is serialized.
  4. Should any information be appended with the serialized object.
  5. Should any further actions be taken after the serialization is done.

With the interceptor, we can...

  1. Determine whether an object or its specific members should be serialized or deserialized.
  2. Alter values during serialization and deserialization.
  3. Trace and debug serialization and deserialization.
  4. Write extra fields and values to the serialized results.

The following part will show how to use the interceptor.

Apply Interceptor to Class with JsonInterceptorAttribute

Given a class like the following code.

C#
[JsonInterceptor (typeof (TestInterceptor))]
public class InterceptorTestSample
{
    public int Value;
    public string Text;
    public bool Toggle;
    public string HideWhenToggleTrue = "Show when toggle false";
    public DateTime Timestamp;
}

When we do serialization, we want the Timestamp field to show the time when the serialization occurs. The HideWhenToggleTrue field to be hidden when the Toggle field is true.

To accomplish the above requirement, we can use the JsonInterceptor<T> class. We firstly annotate the InterceptorTestSample class with the [JsonInterceptor (typeof (TestInterceptor))] attribute.

After that, we implement the IJsonInterceptor interface by overriding the JsonInterceptor<InterceptorTestSample> class. The bold text in the code will fulfill the above requirements.

C#
class TestInterceptor : JsonInterceptor<InterceptorTestSample> {
    public override bool OnSerializing (InterceptorTestSample obj) {
        // we can change the value to be serialized here
        obj.Value = 1;
        Console.WriteLine ("serializing.");
        return true;
    }
    public override void OnSerialized (InterceptorTestSample obj) {
        obj.Value = 2;
        Console.WriteLine ("serialized.");
    }
    public override bool OnSerializing (InterceptorTestSample obj, JsonItem item) {
        Console.WriteLine ("serializing " + item.Name);
        if (item.Name == "Text") {
            obj.Timestamp = DateTime.Now;
            item.Value = "Changed at " + obj.Timestamp.ToString ();
        }
        else if (item.Name == "HideWhenToggleTrue" && obj.Toggle) {
            return false;
        }
        return true;
    }
    public override void OnDeserializing (InterceptorTestSample obj) {
        obj.Value = 3;
        Console.WriteLine ("deserializing.");
    }
    public override void OnDeserialized (InterceptorTestSample obj) {
        obj.Value = 4;
        Console.WriteLine ("deserialized.");
    }
    public override bool OnDeserializing (InterceptorTestSample obj, JsonItem item) {
        Console.WriteLine ("deserializing " + item.Name);
        // we can change the serialized value here
        if (item.Name == "Text") {
            item.Value = "1";
        }
        return true;
    }
}

We use the following code to serialize and deserialize the value.

C#
public void IntercepterTest () {
    var d = new InterceptorTestSample ();
    var s = JSON.ToJSON (d, _JP);
    Console.WriteLine (s);

    var o = JSON.ToObject<InterceptorTestSample> (s);

    d.Toggle = true;
    s = JSON.ToJSON (d, _JP);
    Console.WriteLine (s);
}

And the output of the code is listed below:

JavaScript
serializing.
serializing Value
serializing Text
serializing Toggle
serializing HideWhenToggleTrue
serializing Timestamp
serialized.
{"Value":1,"Text":"Changed at 2015-5-11 14:37:09","Toggle":false,
"HideWhenToggleTrue":"Show when toggle false","Timestamp":"2015-05-11T14:37:09"}
deserializing.
deserializing Value
deserializing Text
deserializing Toggle
deserializing HideWhenToggleTrue
deserializing Timestamp
deserialized.

serializing.
serializing Value
serializing Text
serializing Toggle
serializing HideWhenToggleTrue
serializing Timestamp
serialized.
{"Value":1,"Text":"Changed at 2015-5-11 14:37:09","Toggle":true,"Timestamp":"2015-05-11T14:37:09"}

Apply Interceptor to Class with SerializationManager

The above code shows how to apply the JsonInterceptor to a class with the invasive mode.

Sometimes, we don't have the source code in our hands. The following example will try to conditionally serialize the System.Net.WebException class. Quite obviously, we don't have the source code of the WebException class. We have to use the SerializationManager to control the serialization result of the class. This example also demonstrates how to filter unwanted fields from be serialized.

Firstly, we create the interceptor class. It will order the serialization engine to append two extra name-value pairs to the serialization result, in the SerializeExtraValues method. It also filters the properties to be serialized. In this example, only Status and Message properties are serialized.

C#
public class WebExceptionJsonInterceptor : JsonInterceptor<System.Net.WebException>
{
    // Adds extra values to the serialization result
    public override IEnumerable<JsonItem> SerializeExtraValues (System.Net.WebException obj) {
        return new JsonItem[] {
            new JsonItem ("exceptionTime", DateTime.Now),
            new JsonItem ("machine", Environment.MachineName)
        };
    }
    public override bool OnSerializing (System.Net.WebException obj, JsonItem item) {
        // filter properties
        switch (item.Name) {
            case "Status":
            case "Message":
                // show the above properties
                return true;
            default:
                // hide other properties
                return false;
        }
    }
}

Secondly, we call the OverrideInterceptor method in the SerializationManager to apply the interceptor to the class.

C#
JSON.Manager.OverrideInterceptor<System.Net.WebException> (new WebExceptionJsonInterceptor ());

The following code will give some output:

C#
try {
    var c = System.Net.WebRequest.Create ("http://inexistent-domain.com");
    using (var r = c.GetResponse ()) {
    }
}
catch (System.Net.WebException ex) {
    string s = JSON.ToJSON (ex, p);
    Console.WriteLine (s);
}

The output of the code is listed below (lines wrapped for clarity):

C#
{"httpstatus":"NameResolutionFailure",
"message":"Unable to resolve the host name: 'inexistent-domain.com'",
"exceptionTime":"2015-05-11T06:55:08Z",
"machine":"WMJ"}

What about Performance?

Although many functions have been added to the original fastJSON, however, the performance is never degraded, but improved after many heavy refactors. According to the consoletest benchmark utility supplied in both projects, PowerJSON runs about 5% to 20% faster than the original fastJSON, which is already fast enough.

History

  1. First public release on CodeProject: 2015-4-1
  2. Updated source code file, introduced the new SerializeStaticMembers setting, added description about enum value serialization with JsonConverter<>: 2015-4-3
  3. Breaking change: All extensive attributes are named after Json. Updated source code file, added more description about the upgraded JsonConverter<>: 2015-4-14
  4. Version 2.3.3: Rewrite of this article to reflect the latest change of PowerJSON. A documentation file and compiled DLL files are also attached in the download. 2015-5-11
  5. Version 2.3.4: IJsonConverter can be applied to classes and structs. Performance enhancement. Breaking change: Dictionary<string, object> in primitive types changed to IDictionary<string,object> 2015-5-22
  6. Version 2.3.5: Breaking Change: Convert and Revert method in  JsonConverter<,> changed to be protected to allow the serialized type to be internal types. More customized IJsonConverters added. Marked Serliaze and Deserialize delegates to be obsolete since they can be superceded by IJsonConverter. 2015-5-24
  7. Version 2.4: Makes private members possible to be serialized. Partially supports the DataContractAttribute. Breaking Change: Serializable and Deserializable property of MemberOverride class is changed to nullable boolean values. 2015-7-17
  8. Version 2.5: Supports serializing types which implements IEnumerable. 2015-8-25

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
China China
I am now programming applications for the Internet of Things.

Comments and Discussions

 
QuestionInvalid Cast, no Idea why Pin
Lionel Peeters26-Feb-19 0:30
Lionel Peeters26-Feb-19 0:30 
GeneralMy vote of 5 Pin
MarkBoreham11-Oct-17 2:20
professionalMarkBoreham11-Oct-17 2:20 
Questioninstances of Types 'dynamic, 'PropertyInfo, and "anonymous" structures, will not serialize: correct ? Pin
BillWoodruff23-Feb-16 22:08
professionalBillWoodruff23-Feb-16 22:08 
AnswerRe: instances of Types 'dynamic, 'PropertyInfo, and anonymous methods, will not serialize: correct ? Pin
wmjordan24-Feb-16 1:38
professionalwmjordan24-Feb-16 1:38 
GeneralRe: instances of Types 'dynamic, 'PropertyInfo, and anonymous methods, will not serialize: correct ? Pin
BillWoodruff24-Feb-16 6:56
professionalBillWoodruff24-Feb-16 6:56 
GeneralMy vote of 5 Pin
BillWoodruff23-Feb-16 10:21
professionalBillWoodruff23-Feb-16 10:21 
SuggestionDeserialize 2 json strings into same object Pin
LGSon222-Jan-16 22:32
LGSon222-Jan-16 22:32 
GeneralRe: Deserialize 2 json strings into same object Pin
wmjordan24-Jan-16 19:41
professionalwmjordan24-Jan-16 19:41 
SuggestionA minor suggestion Pin
LGSon219-Oct-15 6:20
LGSon219-Oct-15 6:20 
GeneralRe: A minor suggestion Pin
wmjordan20-Oct-15 18:02
professionalwmjordan20-Oct-15 18:02 
GeneralRe: A minor suggestion Pin
LGSon221-Oct-15 3:24
LGSon221-Oct-15 3:24 
GeneralRe: A minor suggestion Pin
wmjordan21-Oct-15 4:21
professionalwmjordan21-Oct-15 4:21 
GeneralRe: A minor suggestion Pin
LGSon26-Jan-16 5:06
LGSon26-Jan-16 5:06 
GeneralRe: A minor suggestion Pin
wmjordan7-Jan-16 14:07
professionalwmjordan7-Jan-16 14:07 
GeneralRe: A minor suggestion Pin
wmjordan12-Feb-16 3:51
professionalwmjordan12-Feb-16 3:51 
GeneralRe: A minor suggestion Pin
LGSon223-Feb-16 5:31
LGSon223-Feb-16 5:31 
GeneralRe: A minor suggestion Pin
wmjordan23-Feb-16 13:55
professionalwmjordan23-Feb-16 13:55 
QuestionInterfaces Pin
LGSon218-Jun-15 21:51
LGSon218-Jun-15 21:51 
AnswerRe: Interfaces Pin
wmjordan19-Jun-15 16:52
professionalwmjordan19-Jun-15 16:52 
GeneralRe: Interfaces Pin
LGSon219-Jun-15 21:33
LGSon219-Jun-15 21:33 
GeneralRe: Interfaces Pin
wmjordan21-Jun-15 4:04
professionalwmjordan21-Jun-15 4:04 
AnswerRe: Interfaces Pin
wmjordan19-Jun-15 17:26
professionalwmjordan19-Jun-15 17:26 
AnswerRe: Interfaces Pin
wmjordan24-Aug-15 16:25
professionalwmjordan24-Aug-15 16:25 
QuestionVoting of 5 Pin
Ganesan Senthilvel20-May-15 18:13
Ganesan Senthilvel20-May-15 18:13 
AnswerRe: Voting of 5 Pin
wmjordan23-May-15 14:57
professionalwmjordan23-May-15 14:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.