Json: A Fairly Powerful JSON Engine in a Small Package

Updated on 2019-09-05

Use JsonPath, and builtin RPC support to easily communicate with all the JSON / REST services out there, or otherwise manipulate JSON with this little library.

Introduction

This is a small JSON library that provides an in memory object graph for querying and a pull parser for streaming. I used it when I can't justify bloating my code distribution with a full fledged JSON library. It's just enough for many, if not most simple scenarios. It doesn't do anything fancy like JSON schema validation, circular reference resolution, or object mapping (although it supports "dynamic" in C# which accomplishes the same thing).

It has JsonPath support. Those portions of the software are copyright (c) 2007 Atif Aziz. all rights reserved, with portions Copyright (c) 2007 Stefan Goessner (goessner.net), under the MIT license. (I didn't write this bit, but it was well done and small there you go).

Background

As if you didn't already know, JSON is a data interchange format used primarily with web services or as the data format for asynchronous communication between web servers and web browsers. It has effectively supplanted XML in the general sense for these purposes, but that's not to say XML isn't still used, or still useful. This is a lightweight, elegant alternative, basically. It doesn't include schema information, although it's somewhat self describing and easy to parse. XML is drastically more intricate and potentially powerful, but it's also kind of fat and clunky compared to JSON. Plus JavaScript can read JSON natively, which is a huge win for web developers.

If you want specifics, go to json.org - it's a single brief page, and worth your time.

json.org

This is a library for working with it in .NET.

Using the Code

Conceptualizing the In-Memory Object Tree

Typically, you'll simply use the JsonTextReader by instantiating it with some input and then calling ParseSubtree(), or by calling JsonObject.Parse() or JsonObject.LoadFrom(), or JsonObject.LoadFromUrl(). This gets you an in-memory tree of the document represented by nested lists and dictionaries.

The type mappings are as follows:

  • JSON object { } = IDictionary<string,object> (JsonObject)
  • JSON array [ ] = IList<object> (JsonArray)
  • JSON true/false = System.Boolean
  • JSON null = System.Object (null)
  • JSON (numeric) = System.Int32``, System.Int64, System.Numerics.BigInteger, or System.Double depending on what will fit
  • JSON string "..." = System.String
var json = JsonObject.Parse("{\"foo\":\"bar\"}");
json.Add("id", 10);
json["foo"] = "baz";
JsonArray items = new JsonArray();
items.Add("test1");
items.Add("test2");
json["items"] = items;
items.Remove("test2");
Console.WriteLine(json); // pretty print json
return;

Conceptualizing JsonObject and JsonArray

JsonObject and JsonArray are simply thin wrappers around IDictionary<string,object> and IList respectively, with a number of methods specific to dealing with JSON data, most of which are also included as static methods that operate on any IDictionary<string,object> or IList instance. As far as these static methods go, the ones that also deal with other kinds of data, like strings are other scalar values are also on JsonObject.

On JsonObject, of particular interest should be:

  • Select() - returns a set of Json elements based on a JsonPath query
  • Get() - traverses a Json element based on a series of keys and indices and returns the destination
  • CreatePath() - similar to the above, but also creates nodes if they aren't present. Traversing arrays is not yet supported in this method
  • Parse() - parses a JSON string and returns the normalized value
  • LoadFrom() - loads JSON from the specified file
  • LoadFromUrl() - loads JSON from the specified Url
  • WriteTo() - writes JSON to the specified TextWriter
  • SaveTo() - saves JSON to the specified file
  • CopyTo() - copies JSON from one graph to another
  • Adapt() - wraps an existing list with this wrapper, if it's not already wrapped

On JsonArray, we have:

  • ToArray<T>() - various overloads to convert a JSON based array to a "real" array. There's one for scalar types, like a numeric value, and one for custom types, like an entity wrapper. The latter takes a creator function of type Func<object, object> which takes a JSON element and returns T. The return value becomes that member of the array. This allows you to create one entry for each item in the array.
  • Adapt() - wraps an existing list with this wrapper, if it's not already wrapped.

Note that these classes use value semantics, which is to say that they are considered logically equal if they contain the same data.

Anyway, using these requires a bunch of setup to make it real world, so please refer to the included TMDb demo project.

Conceptualizing the JsonTextReader

The JsonTextReader class is a pull parser that allows for streaming access to JSON data. It works very much like Microsoft's XmlReader does, which is to say, it's not exactly friendly (pull parsing never really is), but it's not terrible either. In addition to the standard Read(), NodeType and Value members, it also includes ParseSubtree() which returns the current subtree as a JSON object, SkipSubtree() which quickly skips the subtree, and SkipToField() which advances to the specified field. Use this if you need to bulk process lots of JSON data. I'm not going to spend any more time on it here, as it will hardly be used directly by anyone.

Conceptualizing the JSON/REST Communication with JsonRpc

JsonRpc provides one primary method to facilitate communication with a remote server. The signature follows:

public static object Invoke(
            string baseUrl,
            IDictionary<string, object> args = null,
            object payloadJson = null,
            string timestampField=null,
            string httpMethod = null,
            Func<object,object> fixupResponse=null,
            Func<object, object> fixupError=null,
            JsonRpcCacheLevel cache=JsonRpcCacheLevel.Conservative)
{

As you can see, the catch is it takes quite a few parameters. Let's explore them.

  • baseUrl - indicates the URL to use to make the call, sans any extra query string arguments. You can have query string parameters on this URL - they will be appended to if needed.
  • args - if specified, indicates a simple JSON object with all scalar values that represents the query string arguments.
  • payloadJson - if specified, indicates the JSON payload to send with the request. If httpMethod is not specified, this function will use POST to transmit the payload. The content-type is set to application/json
  • timeStampField - if specified, indicates a field under the root object in which to add a timestamp with the date and time of the request.
  • httpMethod - if specified, indicates a custom HTTP method (like DELETE) to use with the request. Otherwise, GET will be used or POST will be used if there is a payload.
  • fixupResponse - if specified, indicates a callback of the form Func<object,object> that takes and returns normalized JSON. Its purpose is to allow you to perform post-processing on a message received from the server.
  • fixupError - if specified, indicates a callback of the form Func<object,object> that takes and returns normalized JSON. It's purpose is to allow you to perform post-processing on the error message before it is thrown. This is important in order to make sure the exception can return meaningful information. Right now, it uses a simple heuristic to look for a message and an error code. If it can't find it, you can fix the error before it has it, and change the fields around.
  • cacheLevel - indicates the level of caching to be done by the request. This works a lot like a web browser's cache, and only caches GET requests, for obvious reasons. Setting this to Aggressive may dramatically reduce traffic in many cases, but may mean call responses as stale as 5 minutes or so. If set to Conservative, it hits the server with a HEAD request every time, which is fine and even appropriate for an RPC call with some caching, because it ensures you'll never get a stale response. However, it only really saves some bandwidth, not necessarily latency, although the HTTP pipelining used should mitigate this somewhat. If set to MachinePolicy, it uses the settings from machine.config.

This method may look daunting but the reality is you'll almost never use all these parameters at once. In fact, for most of the calls, you'll pass the first two parameters. Here's the call to retrieve the configuration from themoviedb.org API.

themoviedb.org

var args = new JsonObject();
args.Add("api_key", "c83a68923b7fe1d18733e8776bba59bb");
var json = JsonRpc.Invoke("https://api.themoviedb.org/3/configuration", args);
Console.WriteLine(json);

See? That wasn't so hard. The result is a JSON element, usually an object IDictionary<string,object>/JsonObject or sometimes an IList/JsonArray.

For more, see the extensive "demo" app (a project in its own right).

History

  • 5th September, 2019 - Initial submission