Introduction
I know that there are several Json parsers for C# but I like to understand stuff by doing it on my own. So when I needed to explore some server output in Json format and considering that Json really is a very simple structure, I decided so create a custom parser reading it. This is the core of my tool to show Json data in a tree view.
In addition, I wanted to be able to (de-)serialize Json data from and into C# classes. Thus I wrote a custom serialization class which can handle different scenarios without a whole bunch of initialization or attributes.
Background
Json example
This is just an example to show what Json data looks like - this is formatted, when using server output as less white chars as possible might be used:
{
"PossibleValues" : {
"ObjectValue" : {
"OnlyValue" : "This is the only value in here"
},
"ArrayValue" : [
1, 1, 2, 3, 5, 8, 13, 21, "..."
],
"StringValue" : "String",
"NumberValue" : 3.1415926536,
"BooleanValue" : true,
"NullValue" : null
},
"Lightspeed" : {
"Value" : 2.99792458e8,
"Unit" : "m/s"
}
}
Representing the Json structure
Json supports six types of data:
- Object: Is enclosed in bracelets '
{ }
' and has name-value content where names and values are divided by a colon ':
' and name-value pairs are divided by comma ',
'. In code this will be represented with a BjSJsonObject
class instance implementing a BjSJsonObjectValue
List to represent the name-value pairs. - Array: Is enclosed in brackets '
[ ]
' and has values as content divided by comma ',
'. In code this will be represented with a BjSJsonArray
class instance implementing an object
List. - String: Is enclosed by quotes '
"
'. In code this is a String
. - Number: Is something like
-1.454324e-18
or a simpler form of that. In code this is represented as a Decimal
so that a maximum precision is kept. - Boolean: Is the word '
true
' or 'false
' without quotes. In code this is a Boolean
. - Null: Is the word '
null
' without quotes. In code this is null
.
A Json object as well as an array can contain different types of data so that a name-value pairs and the arrays only contain C# object
s storing the actual values. To determine which kind of value is stored you can use the is
operator or an enumerated value description as BjSJsonValueKind
from the helper function BjSJsonHelper.GetValueKind(...)
.
The Json parser state machine
The parser is a state machine which reads the input char by char. It's pretty straight forward switching states whenever a new part begins, so I won't go any deeper here - the rest can be seen in the code at BjSJsonHelper.BjSJsonReader
.
The Json (De-)Serializer
This basically has only two public methods:
ToJson<T>(T obj)
: Serializes a class of a generic Type into a BjSJsonObject - all subtypes, arrays, Lists and Dictionaries are handled automatically. FromJson<T>(BjSJsonObject obj)
: Maps a BjSJsonObject into an instance of the generic Type T - here all subtypes, arrays, Lists and Dictionaries are handled automatically, too.
The serializing method ToJson<T>(T obj)
loops through the properties of given Type of T and tries to extract each value and to convert it to a Json representation.
The deserializing method FromJson<T>(BjSJsonObject obj)
loops through the properties of the BjSJsonObject and tries to find a Property in the given Type of T with the same name and an appropriate data type. If that is found, the Json value is converted into the data type of the class property.
There are some limitations though as I manually manage the serialization of data types. So this serializer only supports classes with a standard constructor (also for subclasses) and only properties with the following data types:
null
String
Numbers
(byte, sbyte, short, ushort, int, uint, long, ulong, float, double and decimal) Guid
DateTime
(is converted into an ISO formatted string) TimeSpan
(is converted into a formatted string) Image
|Bitmap
(is converted into a Base64 string) Array
(one-dimentional, all T[] where T is one of these data types) List<T>
(where T is one of there data types, is converted into an array - the type definition of the property separates between Array and List when deserializing) Dictionary<K,V>
(where K and V are each one of these data types, is converted into an array of two-value arrays) class
instance (with standard constructor and only properties of these data types)
Other data types I tried to skip - but I haven't tested that as these are all data types I need to use.
Using the code
The main class used for handling Json objects is BjSJsonObject
. BjSJsonObjectMember
and BjSJsonArray
are used within BjSJsonObject and build up the data types which are not represented by simple C# classes. The BjSJsonConverter
class can be used to convert C# classes into Json objects and back.
The usage is pretty staight forward. To load a Json obect just use one of the construcors:
string data = "{\"Member\":\"Value\",\"Another\":3.14}";
BjSJsonObject jObj = new BjSJsonObject(data);
string filename = @"C:\data.json";
BjSJsonObject jObj = new BjSJsonObject(filename, Encoding.ASCII);
MemoryStream ms = new MemoryStream(File.ReadAllBytes(filename));
BjSJsonObject jObj = new BjSJsonObject(new StreamReader(ms));
To turn it back to Json text just call the ToJsonString(bool) method:
BjSJsonObject jObj = new BjSJsonObject(@"C:\data.json", Encoding.ASCII);
string jsonData = jObj.ToJsonString(true);
string jsonData = jObj.ToJsonString(false);
You can access and alter data by using the indexes, Add(), Remove() and RemoveAt() methods. Also BjSJsonObject and BjSJsonArray implement the interface IEnumerator and offer a Count property, so that for and foreach loops can also be used. The following example shows how a BjSJsonObject is created, filled with data that is used to add other properties before the result is saved to a file:
BjSJsonObject jObj = new BjSJsonObject();
BjSJsonArray jArr = new BjSJsonArray();
for (int i = 0; i < 10; i++)
jArr.Add(Convert.ToDecimal(i));
jObj.Add("arr", jArr);
foreach (object n in jArr)
jObj.Add(n.ToString(), (decimal)n * (decimal)n);
string jData = jObj.ToJsonString(false);
File.WriteAllText(@"C:\output.json", jData);
The BjSJsonConverter can only be used very straight forward - as intended. Passed objects are handled recursively. Besides the object itself the only other information used is the objects type. For converting to Json this is only used to have the same method pattern as for when converting back. The following exaple defines and creates an instance of the Customer class, converts is to Json text and back into a second class instance:
public class Customer
{
public Guid Id { get; set; }
public string Name { get; set; }
public Dictionary<string, Address> Addresses { get; set; }
public List<Customer> SubCustomers { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
public void DoConverting()
{
Customer c1 = new Customer()
{
Id = Guid.NewGuid(),
Name = "Max Mustermann",
Addresses = new Dictionary<string, Address>()
{
{ "Work", new Address() { Street = "Bahnhofstr. 1", City = "12345 Neustadt" },
{ "Home", new Address() { Street = "Postweg 34", City = "12345 Neustadt" }
},
SubCustomers = new List<Customer>()
{
new Customer()
{
Id = Guid.NewGuid(),
Name = "Tanja Müller",
Addresses = null,
SubCustomers = null
},
new Customer()
{
Id = Guid.NewGuid(),
Name = "Steffan Taylor",
Addresses = null,
SubCustomers = null
}
}
}
string jText = BjSJsonConverter.ToJson(c1).ToJsonString(true);
BjSJsonObject jObj = new BjSJsonObject(jText);
Customer c2 = BjSJsonConverter.FromJson<Customer>(jObj);
}
The JsonViewer project is only a test scenario and a tool I use.
Points of Interest
The conversion code in the BjSJsonConverter is pretty dynamic. It could easily be used for other purposes like a ORM. I definitely recommend to have a look at it to anyone how wants to start working on .NET Reflection.