Search code examples
c#jsonsystem.text.json

How to add a new property to JsonElement using System.Text.Json?


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?


Solution

  • 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.