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, JsonArray
as an array and JsonValue
for a value in JSON.
We start with JsonValue
and build our way up. The JsonValue
is 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);
You can always be explicit about the types too,
var identifier = JsonValue.Create<Guid>(Guid.NewGuid());
var yearAtDev = JsonValue.Create<int>(5);
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
}
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"
}
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();
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 JsonArray
except 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"));
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 JsonNode
in System.Text.Json
could be very well be substituted by JsonValue or JsonObject
i.e. you can add either of the types through the Add
method on the JsonArray
But please feel free to correct me here and add any relevant use case with JsonArray
if 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"}
};
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
}
}
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;
}
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 ?
}
}
Following is an extract from the .NET source code, and as you see an InvalidOperationException
is going to get thrown if you try to call AsValue()
- say for example on a JsonObject
( i.e. JsonNode
which is of type JsonObject
) . AsValue()
would only yield a JsonValue
on being called on a JsonNode
which 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;
}
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)