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.
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);
return;
Conceptualizing JsonObject and JsonArray
JsonObject
and JsonArray
are simply thin wrappers around IDictionary<string,object>
and IList<object>
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<object>
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.
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<object>
/JsonArray
.
For more, see the extensive "demo" app (a project in its own right).
History
- 5th September, 2019 - Initial submission