DEV Community

Vidisha Parab
Vidisha Parab

Posted on • Edited on

Working with JsonObject, JsonNode, JsonValue and JsonArray [System.Text.Json API]

Most of the articles which I found on the web about the System.Text.Json API dealt with serialization and de-serialization with little being explored about creating a JSON entity on the fly.
A recent use-case required us to build a data entity dynamically while running through a set of business logic (emphasis on the fact that the output here could not be modelled as a POCO since it was going to take form during the run-time). This entity would be serialized and sent over to a downstream service. While one could opt for the Dynamic Objects, we decided to work our way with JSON objects.

When working with dynamic data through the System.Text.Json API, everything revolves around the JsonNode class. It is an abstract class and is derived by the JsonArray, the JsonObject and the JsonValue. These can be thought of as the building blocks - we work with them more in the coming examples.

Before we actually dive into the System.Text.Json API, let's take a step back and re-visit the JSON standard.

The json.org quotes the following

JSON is built on two structures:
A collection of name/value pairs. In various languages, this is realized as an object, record, struct, dictionary, hash table, keyed list, or associative array.
An ordered list of values. In most languages, this is realized as an array, vector, list, or sequence.

It then elaborates how in JSON, this could take two forms - either an object and an array.

  • An object and an array in JSON can have a JSON value

  • A value in JSON can also in turn be an array, a number a object , etc

We kind of get an idea that a json value, json object and the json array are all intertwined.

Going back to System.Text.Json API, we have the JsonObject which can be used to realise an object in JSON, JsonArrayas an array and JsonValuefor a value in JSON.

We start with JsonValue and build our way up. The JsonValueis an abstract class (you cannot instantiate an abstract class). The next obvious thought is - how are we then going to realise a C# type here without being able to instantiate the type ? Our C# counterpart could be anything string, double, int, etc.

The JsonValue class contains static methods (Create) for the C# value types and based on in-built implicit conversion, they are going to give you a type of JsonNode for the C# value you provided.

var firstName = JsonValue.Create("Doofus");
var lastName = JsonValue.Create("Flintstones");
var addressPostcode = JsonValue.Create(1234);
var phoneNumber= JsonValue.Create(1234567890);
var knowsCSharp= JsonValue.Create(true);
Enter fullscreen mode Exit fullscreen mode

You can always be explicit about the types too,

var identifier = JsonValue.Create<Guid>(Guid.NewGuid());
var yearAtDev = JsonValue.Create<int>(5);
Enter fullscreen mode Exit fullscreen mode

You might wonder what's the advantage here ? Also we said we want a JsonValue but we are getting back a JsonNode - it will make sense in a while but for now what we can assert here is the type definition.

As of .NET 8, through System.Text.Json API we also get JsonValueKind

public enum JsonValueKind : byte
{
 Undefined = 0,
 Object = 1,
 Array = 2,
 String = 3,
 Number = 4,
 True = 5,
 False = 6,
 Null = 7
}
Enter fullscreen mode Exit fullscreen mode

While working with serialization and deserialization of data, the widely adopted standard is JSON.

JsonValue.Create<int>(5) would create a JSON value of kind number and
JsonValue.Create<string>("5") would create a JSON value of kind string. This would matter when this data would be deserialized in any downstream system

{
 "age" : 5,
 "age2" : "5"

}
Enter fullscreen mode Exit fullscreen mode

would be deserialized differently, age would have to be deserialized as an integer and age2 as a string. Hence it is good to assert type definitions while building up the json data at the source and through the different factory method overloads (can also be achieved with the implicit conversions) , System.Text.Json API provides a cleaner way

We can also use the GetValueKind method to get the JsonValueKind

   var guid = JsonValue.Create<Guid>(Guid.NewGuid());
   var jsonValueKind =  guid.GetValueKind();
Enter fullscreen mode Exit fullscreen mode

Now that we have a fair understanding of the JsonValue, we try to use that to realise a JsonArray.

A JsonArray is but a collection of json values or json objects.

To be completely honest, I haven't explored much with JsonArrayexcept for a deserialization use-case. However I skimmed through the documentation and here is one sample to create a JSON array
JsonArray is a sealed class, you cannot inherit it but you can instantiate it.

   var jsonArray = new JsonArray();
   jsonArray.Add(JsonValue.Create(23));
   jsonArray.Add(JsonValue.Create("23"));
Enter fullscreen mode Exit fullscreen mode

The Add method on the JsonArray accepts a parameter of type JsonNode and if you recall the definition from the json.org , a Json array could consist of a JSON object or a JSON value - this blends quiet well here, that a JsonNodein System.Text.Json could be very well be substituted by JsonValue or JsonObject i.e. you can add either of the types through the Addmethod on the JsonArray

But please feel free to correct me here and add any relevant use case with JsonArrayif you know of :)

Moving on to the JsonObject,

JsonObject is basically a collection KeyValuePair of string and JsonNode ! It derives from the JsonNode and well holds a JsonNode too.

In the beginning, the JsonNode had got me all confused but if you take a moment and appreciate the tree data structure and think of JSON as one, then a tree is just a collection of nodes. With JSON, the different entities are a value, object and an array so a node could be any one of these ? and may be then it makes sense that the JsonValue, JsonArray and JsonObject derive from the JsonNode . Anyway, I like to look at it that way.

With JsonObject then its just putting together the json values, arrays or even nested objects

  var jsonObject = new System.Text.Json.Nodes.JsonObject()
  {

   ["firstName"] = JsonValue.Create("Doofus"),
   ["lastName "] = JsonValue.Create("Flintstones"),
   ["address"] = new System.Text.Json.Nodes.JsonObject
   {
    ["line1"]= "Stockholm",
    ["line2"]= "Sweden"
   },
   ["knowsCSharp"] = JsonValue.Create(true),
   ["yearsAtDev "] = JsonValue.Create<int>(5),
   ["hobbies"] = new JsonArray() { "music", "dance"}

  };

Enter fullscreen mode Exit fullscreen mode

Also now might be a good time to look at how we could work with deserialization here

Usually when we get data over the wire, we get a JSON. In our consumer when we get a JSON response (say through a REST call) , we deserialize it in a POCO

{
  "FirstName": "Doofus",
  "LastName": "Flintstones",
  "Address": {
    "street": {
      "line1": "Stockholm",
      "line2": "Sweden",
      "line3": "Nordic"
    },
    "postcode": 1234
  }
}

Enter fullscreen mode Exit fullscreen mode

the POCO could be modelled as


class Person
{
      public string FirstName { get; set; } = string.Empty;
      public string LastName { get; set; } = string.Empty;
      public Address? Address { get; set; }


}

public class Address
{
      public Street? Street { get; set; }
      public int Postcode { get; set; }
}

public class Street
{
      public string Line1 { get; set; } = string.Empty;
      public string Line2 { get; set; } = string.Empty
      public string Line3 { get; set; } = string.Empty;
}

Enter fullscreen mode Exit fullscreen mode

and we get the deserialized data by doing -

var person = System.Text.Json.JsonSerializer.Deserialize<Person>(jsonResponseAsString);

But what if we instead deserialize into a JsonObject and not a POCO ?

var personAsJsonObject = System.Text.Json.JsonSerializer.Deserialize<JsonObject>(jsonResponseAsString);

If you have used Newtonsoft before, you might be familiar with the JObject and we used an indexer to traverse it.

A JsonObject if you recall is a key value pair, the value being of type JsonNode

If we want to access the property FirstName then we could either use the indexer

personAsJsonObject["firstName"] or personAsJsonObject.TryGetPropertyValue("firstName", out var value)

Both are going to give you a JsonNode, but a JsonNode could be either a JsonValue,a JsonArray or JsonObject how would you know ?

On JsonNode, we can call AsValue(), AsArray() and AsObject() to get the corresponding type. Next, we could also assert the kind through GetValueKind and call any one of the factory methods we saw earlier to retrieve our value.

I did it very old-school way, something on the lines of this , you can have one each for JsonObject and JsonArray as well

public static JsonValue ToJsonValue(JsonNode node)
{
    try
    {
        return node.AsValue();
    }
    catch (InvalidOperationException)
    {
      //handle it at the caller level by letting it fall-back to 
      next in chain , perhaps JsonArray ?
    }
}
Enter fullscreen mode Exit fullscreen mode

Following is an extract from the .NET source code, and as you see an InvalidOperationExceptionis going to get thrown if you try to call AsValue() - say for example on a JsonObject( i.e. JsonNodewhich is of type JsonObject) . AsValue() would only yield a JsonValueon being called on a JsonNodewhich holds a JsonValue, for others we get an exception

    public JsonValue AsValue()
    {
        JsonValue? jValue = this as JsonValue;

        if (jValue is null)
        {
            ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonValue));
        }

        return jValue;
    }
Enter fullscreen mode Exit fullscreen mode

Next after you have retrieved a JsonValue, you can call the nice helper methods to retrieve a string, boolean, int, etc.

jsonValue.TryGetValue(out string? value)

While there is a lot of ground to cover here and also to learn, even for me ..I hope this serves as a starting point/ basics when you in future would want to explore this path

Any suggestions/improvements welcome.

Top comments (0)