'Equivalent of JObject in System.Text.Json

I have DTO class that has a property of type JObject. This DTO class is send/receive over HTTP between multiple services. JObject is used because the ExtractedData does not have predefined properties

public class MyDTO
{
    public JObject ExtractedData {get;set;}
}

I am converting this project to .NET 5. What is equivalent to JObject in .NET 5? I am trying to avoid JsonDocument because (from the docs):

JsonDocument builds an in-memory view of the data into a pooled buffer. Therefore, unlike JObject or JArray from Newtonsoft.Json, the JsonDocument type implements IDisposable and needs to be used inside a using block.

I am planing to use JsonElement. Is this the most appropriate choice or is there any other type available to hold JSON as an object?



Solution 1:[1]

In .NET 5 and .NET Core 3.1 the closest equivalent to JObject is indeed JsonElement so you could modify your DTO as follows:

public class MyDTO
{
    public JsonElement ExtractedData {get;set;}
}

There is no need to worry about disposing of any documents as, internally, the JsonElementConverter used by JsonSerializer returns a non-pooled element (by cloning the element in .NET 5).

However, the correspondence is not exact, so keep the following in mind:

  1. JsonElement represents any JSON value and thus corresponds most closely to JToken not JObject. As JsonElement is a struct there is no subclass corresponding to a JSON object. If you want to constrain ExtractedData to be a JSON object you will need to check this in the setter:

    public class MyDTO
    {
        JsonElement extractedData;
    
        public JsonElement ExtractedData
        {
            get => extractedData;
            set
            {
                if (value.ValueKind != JsonValueKind.Object
                    // && value.ValueKind != JsonValueKind.Null Uncomment if you want to allow null
                    )
                    throw new ArgumentException(string.Format("{0} is not a JSON object type", value.ValueKind));
                extractedData = value;
            }
        }
    }
    
  2. Since JsonElement is a struct, the default value is not null. So, what is it? It turns out that default(JsonElement) has ValueKind = JsonValueKind.Undefined:

    There is no value (as distinct from Null).

    If you attempt to serialize such a default JsonElement with JsonSerializer, an exception will be thrown. I.e. if you simply do

    var json = JsonSerializer.Serialize(new MyDTO());
    

    Then a System.InvalidOperationException: Operation is not valid due to the current state of the object. exception is thrown.

    You have a few options to avoid this problem:

    • In .NET 5 you can apply [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] like so:

      public class MyDTO
      {
          [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
          public JsonElement ExtractedData {get;set;}
      }
      

      This causes uninitialized values of ExtractedData to be skipped during serialization.

    • In .NET Core 3.x JsonIgnoreCondition does not exist, so you could instead define ExtractedData to be nullable:

      public class MyDTO
      {
          public JsonElement? ExtractedData {get;set;}
      }
      

      Or you could initialize it to a null JsonElement like so:

      public class MyDTO
      {
          public JsonElement ExtractedData {get;set;} = JsonExtensions.Null;
      }
      
      public static class JsonExtensions
      {
          static readonly JsonElement nullElement = CreateNull();
      
          public static JsonElement Null => nullElement;
      
          static JsonElement CreateNull()
          {
              using var doc = JsonDocument.Parse("null");
              return doc.RootElement.Clone();
          }
      }
      

      Both options cause uninitialized values of ExtractedData to serialize as null.

  3. See also the related questions:

Solution 2:[2]

As of Nov 2021, .NET 6 introduces the System.Text.Json.Nodes namespace which:

Provides types for handling an in-memory writeable document object model (DOM) for random access of the JSON elements within a structured view of the data

The four new types are JsonArray, JsonObject, JsonNode and JsonValue. The closest type to JObject is JsonObject which offers similar functionality.

See below for some examples:

// create object manually using initializer syntax
JsonObject obj = new JsonObject
{
    ["Id"] = 3,
    ["Name"] = "Bob",
    ["DOB"] = new DateTime(2001, 02, 03),
    ["Friends"] = new JsonArray
    {
        new JsonObject 
        {
            ["Id"] = 2,
            ["Name"] = "Smith"
        },
        new JsonObject
        {
            ["Id"] = 4,
            ["Name"] = "Jones"
        } 
    }
};

// random access to values
int id = (int)obj["Id"];
DateTime dob = (DateTime)obj["DOB"];
string firstFriendName = (string)obj["Friends"][0]["Name"];

Some other cool things which now make using System.Text.Json much easier in .NET6 are listed below.

Parse, Create, and DOM Manipulation

// parse
var jsonObj = JsonNode.Parse(jsonString).AsObject();

If you have a JsonElement (perhaps after deserializing into dynamic, object, or JsonElement) you can call Create, now you have a navigable and writable DOM object:

// create
JsonObject obj = JsonObject.Create(jsonElement);

You can Add/Remove properties:

obj.Add("FullName", "Bob Smith");
bool successfullyRemoved = obj.Remove("Name");

Safely interrogate object for particular key using ContainsKey and TryGetPropertyValue (which returns a JsonNode):

if (obj.ContainsKey("Hobbies"))
    // do stuff

if (obj.TryGetPropertyValue("Hobbies", out JsonNode? node))
    // do stuff with node

Project and Filter data

It's possible to use Linq to project and filter the JsonObject:

// select Keys
List<string> keys = obj.Select(node => node.Key).ToList();
// filter friends
var friends = obj["Friends"].AsArray()
             .Where(n => (int)n.AsObject()["Id"] > 2);

Deserialize Json

It's now easy to deserialize the Json or deserialize a portion of the Json. This is useful when we only want to deserialize partial Json from the main object. For the example above we can deserialize the list of friends into a generic List<Friend> easily:

List<Friend> friends = obj["Friends"].AsArray().Deserialize<List<Friend>>();

where Deserilize<T>() is an extension method on JsonNode.

Serialize

It's easy to serialize the JsonObject by using ToJsonString():

string s = obj.ToJsonString();

// write pretty json with WriteIndented
string s = obj.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));

In your particular case you could define JsonObject in your DTO:

public class MyDTO
{
    public JsonObject ExtractedData {get;set;}
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2