3

Let's say I have Dictionary<string, object> metas. The object could be JsonElement (System.Text.Json)

And I need iterate each element if the object is an array of elements, and add new property to each element.

I can iterate elements like this:

var metaValue = metas["key-1"];

if (metaValue is JsonElement elementValue && elementValue.ValueKind == JsonValueKind.Array)
 {
    // Iterate over the elements in the array
    foreach (var element in elementValue.EnumerateArray())
    {
        // add new property to element
        // like element["newproperty"] = "test value"
    }
 }

My question is, how to add new property to JsonElement?

dbc
  • 104,963
  • 20
  • 228
  • 340
Youxu
  • 1,050
  • 1
  • 9
  • 34
  • How was your `Dictionary metas` constructed? Was it constructed by deserializing JSON to a `Dictionary` e.g. as follows: `var metas = JsonSerializer.Deserialize>(json)`? – dbc Jun 10 '23 at 14:23
  • Also, what version of .NET are you using? .NET 6, later, or earlier? – dbc Jun 10 '23 at 14:28
  • I am using .net 6. the dictionary is a property of a class MyType, which deserialized from json usine JsonSerializer.Deserialize(..) – Youxu Jun 11 '23 at 22:10
  • Then, as suggested in my answer. I would deserialize with the option `JsonSerializerOptions.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode`. That way your dictionary values will be mutable `JsonNode` objects rather than immutable `JsonElement` objects. – dbc Jun 11 '23 at 23:02

1 Answers1

1

You cannot modify JsonElement, it is completely immutable. Instead, you must deserialize it to some mutable type such as JsonNode, modify the mutable type, then re-serialize back to JsonElement. The following method does that:

public static class JsonExtensions
{
    public static JsonNode TryAddPropertyToArrayElements<TProperty>(this JsonNode node, string name, TProperty value)
    {
        if (node is JsonArray array)
            foreach (var obj in array.OfType<JsonObject>())
                obj[name] = JsonSerializer.SerializeToNode(value);
        return node;
    }

    public static JsonElement TryAddPropertyToArrayElements<TProperty>(this JsonElement element, string name, TProperty value) =>
        element.ValueKind == JsonValueKind.Array
        ? JsonSerializer.SerializeToElement(JsonSerializer.Deserialize<JsonNode>(element)!.TryAddPropertyToArrayElements(name, value))
        : element;

    public static object? TryAddPropertyToArrayElements<TProperty>(this object? obj, string name, TProperty value) =>
        obj switch
        {
            JsonElement e => e.TryAddPropertyToArrayElements(name, value),
            JsonNode n => n.TryAddPropertyToArrayElements(name, value),
            null => null, // JSON values that are null are deserialized to the c# null value, not some element or node of type null
            _ => throw new ArgumentException("Unexpected type ${obj}"),
        };
}

Then you would use it as follows:

metas["key-1"] = metas["key-1"]?
    .TryAddPropertyToArrayElements("newproperty", "test value");

Demo fiddle #1 here.

Obviously all this serialization is somewhat inefficient. To avoid the extra serialization, assuming you generated your dictionary via deserialization, you could deserialize with JsonSerializerOptions.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode. This setting forces types declared as object to be deserialized to JsonNode rather than JsonElement:

var inputOptions = new JsonSerializerOptions
{
    UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode,
};
var metas = JsonSerializer.Deserialize<Dictionary<string, object?>>(json, inputOptions)!;

If you do you will be able to mutate its values directly using the method JsonExtensions.TryAddPropertyToArrayElements<TProperty>(this JsonNode node, string name, TProperty value) included above.

Notes:

  • JsonNode and JsonSerializer.SerializeToElement() were introduced in .NET 6 so if you are using an earlier version you will need to use a different approach.

Demo fiddle #2 here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thanks dbc! Your answer worked perfectly! Just more question, in some case I just want to assign the new added property the value of an existing property with this code jsonObject["newproperty"] = JsonObject["existingProperty"]; // jsonObject is item of JsonArray which deserialized with your approach. Then I got this error : System.InvalidOperationException: 'The node already has a parent. What does this mean? – Youxu Jun 12 '23 at 16:22
  • @Youxu - you're welcome. The *`The node already has a parent`* error happens because `JsonNode` objects form doubly-connected graphs: parents know their children and children know their parents. So when you assign a child that already has a parent to a different parent, the library would need to do something such as remove it from the old parent, or clone it. This isn't implemented, hence the error. See [Clone a JsonNode and attach it to another one in .NET 6](https://stackoverflow.com/q/71570877/3744182). – dbc Jun 12 '23 at 16:28
  • Thanks @dbc! marked the answer as answered. – Youxu Jun 12 '23 at 20:08