System.Text.Json 合并两个对象

System.Text.Json Merge two objects

是否可以像这样将两个 json 对象与 System.Text.Json?

合并

对象 1

{
   id: 1
   william: "shakespeare"
}

对象 2

{
   william: "dafoe"
   foo: "bar"
}

结果对象

{
    id: 1
    william: "dafoe"
    foo: "bar"
}

我可以像这样newtonsoft.json实现

var obj1 = JObject.Parse(obj1String);
var obj2 = JObject.Parse(obj2String);

obj1.Merge(obj2);
result = settings.ToString();

但是 System.Text.Json 有办法吗?

自 .Net Core 3.0 起 JSON 对象的合并未由 System.Text.Json:

实现

更一般地说,JsonDocument只读的。它

Provides a mechanism for examining the structural content of a JSON value without automatically instantiating data values.

因此,它不支持以任何方式修改 JSON 值,包括将另一个 JSON 值合并到其中。

目前有一个增强请求来实现可修改的 JSON 文档对象模型: Issue #39922: Writable Json DOM. It has an associated specification Writable JSON Document Object Model (DOM) for System.Text.Json. If this enhancement were implemented, merging of JSON documents would become possible. You could add an issue requesting functionality equivalent to JContainer.Merge(),链接回问题 #39922 作为先决条件。

System.Text.Json 请求此功能已经存在问题:https://github.com/dotnet/corefx/issues/42466

与此同时,您可以基于 Utf8JsonWriter 编写自己的 Merge 方法作为解决方法(因为现有的 JsonDocumentJsonElement API 是读取-仅)。

如果您的 JSON 对象 仅包含非空 simple/primitive 值 并且属性显示的顺序不是特别重要,则以下相对简单的代码示例应该适合您:

public static string SimpleObjectMerge(string originalJson, string newContent)
{
    var outputBuffer = new ArrayBufferWriter<byte>();

    using (JsonDocument jDoc1 = JsonDocument.Parse(originalJson))
    using (JsonDocument jDoc2 = JsonDocument.Parse(newContent))
    using (var jsonWriter = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = true }))
    {
        JsonElement root1 = jDoc1.RootElement;
        JsonElement root2 = jDoc2.RootElement;

        // Assuming both JSON strings are single JSON objects (i.e. {...})
        Debug.Assert(root1.ValueKind == JsonValueKind.Object);
        Debug.Assert(root2.ValueKind == JsonValueKind.Object);

        jsonWriter.WriteStartObject();

        // Write all the properties of the first document that don't conflict with the second
        foreach (JsonProperty property in root1.EnumerateObject())
        {
            if (!root2.TryGetProperty(property.Name, out _))
            {
                property.WriteTo(jsonWriter);
            }
        }

        // Write all the properties of the second document (including those that are duplicates which were skipped earlier)
        // The property values of the second document completely override the values of the first
        foreach (JsonProperty property in root2.EnumerateObject())
        {
            property.WriteTo(jsonWriter);
        }

        jsonWriter.WriteEndObject();
    }

    return Encoding.UTF8.GetString(outputBuffer.WrittenSpan);
}

Newtonsoft.Json 在进行合并时有不同的 null 处理,其中 null 不会覆盖非空 属性 的值(当有重复项时) .我不确定你是否想要这种行为。如果需要,您需要修改上述方法来处理 null 情况。以下是修改:

public static string SimpleObjectMergeWithNullHandling(string originalJson, string newContent)
{
    var outputBuffer = new ArrayBufferWriter<byte>();

    using (JsonDocument jDoc1 = JsonDocument.Parse(originalJson))
    using (JsonDocument jDoc2 = JsonDocument.Parse(newContent))
    using (var jsonWriter = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = true }))
    {
        JsonElement root1 = jDoc1.RootElement;
        JsonElement root2 = jDoc2.RootElement;

        // Assuming both JSON strings are single JSON objects (i.e. {...})
        Debug.Assert(root1.ValueKind == JsonValueKind.Object);
        Debug.Assert(root2.ValueKind == JsonValueKind.Object);

        jsonWriter.WriteStartObject();

        // Write all the properties of the first document that don't conflict with the second
        // Or if the second is overriding it with null, favor the property in the first.
        foreach (JsonProperty property in root1.EnumerateObject())
        {
            if (!root2.TryGetProperty(property.Name, out JsonElement newValue) || newValue.ValueKind == JsonValueKind.Null)
            {
                property.WriteTo(jsonWriter);
            }
        }

        // Write all the properties of the second document (including those that are duplicates which were skipped earlier)
        // The property values of the second document completely override the values of the first, unless they are null in the second.
        foreach (JsonProperty property in root2.EnumerateObject())
        {
            // Don't write null values, unless they are unique to the second document
            if (property.Value.ValueKind != JsonValueKind.Null || !root1.TryGetProperty(property.Name, out _))
            {
                property.WriteTo(jsonWriter);
            }
        }

        jsonWriter.WriteEndObject();
    }

    return Encoding.UTF8.GetString(outputBuffer.WrittenSpan);
}

如果您的 JSON 对象可能包含嵌套的 JSON 值,包括其他对象和数组 ,您可能希望扩展逻辑来处理该问题.这样的事情应该有效:

public static string Merge(string originalJson, string newContent)
{
    var outputBuffer = new ArrayBufferWriter<byte>();

    using (JsonDocument jDoc1 = JsonDocument.Parse(originalJson))
    using (JsonDocument jDoc2 = JsonDocument.Parse(newContent))
    using (var jsonWriter = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = true }))
    {
        JsonElement root1 = jDoc1.RootElement;
        JsonElement root2 = jDoc2.RootElement;

        if (root1.ValueKind != JsonValueKind.Array && root1.ValueKind != JsonValueKind.Object)
        {
            throw new InvalidOperationException($"The original JSON document to merge new content into must be a container type. Instead it is {root1.ValueKind}.");
        }

        if (root1.ValueKind != root2.ValueKind)
        {
            return originalJson;
        }

        if (root1.ValueKind == JsonValueKind.Array)
        {
            MergeArrays(jsonWriter, root1, root2);
        }
        else
        {
            MergeObjects(jsonWriter, root1, root2);
        }
    }

    return Encoding.UTF8.GetString(outputBuffer.WrittenSpan);
}

private static void MergeObjects(Utf8JsonWriter jsonWriter, JsonElement root1, JsonElement root2)
{
    Debug.Assert(root1.ValueKind == JsonValueKind.Object);
    Debug.Assert(root2.ValueKind == JsonValueKind.Object);

    jsonWriter.WriteStartObject();

    // Write all the properties of the first document.
    // If a property exists in both documents, either:
    // * Merge them, if the value kinds match (e.g. both are objects or arrays),
    // * Completely override the value of the first with the one from the second, if the value kind mismatches (e.g. one is object, while the other is an array or string),
    // * Or favor the value of the first (regardless of what it may be), if the second one is null (i.e. don't override the first).
    foreach (JsonProperty property in root1.EnumerateObject())
    {
        string propertyName = property.Name;

        JsonValueKind newValueKind;

        if (root2.TryGetProperty(propertyName, out JsonElement newValue) && (newValueKind = newValue.ValueKind) != JsonValueKind.Null)
        {
            jsonWriter.WritePropertyName(propertyName);

            JsonElement originalValue = property.Value;
            JsonValueKind originalValueKind = originalValue.ValueKind;

            if (newValueKind == JsonValueKind.Object && originalValueKind == JsonValueKind.Object)
            {
                MergeObjects(jsonWriter, originalValue, newValue); // Recursive call
            }
            else if (newValueKind == JsonValueKind.Array && originalValueKind == JsonValueKind.Array)
            {
                MergeArrays(jsonWriter, originalValue, newValue);
            }
            else
            {
                newValue.WriteTo(jsonWriter);
            }
        }
        else
        {
            property.WriteTo(jsonWriter);
        }
    }

    // Write all the properties of the second document that are unique to it.
    foreach (JsonProperty property in root2.EnumerateObject())
    {
        if (!root1.TryGetProperty(property.Name, out _))
        {
            property.WriteTo(jsonWriter);
        }
    }

    jsonWriter.WriteEndObject();
}

private static void MergeArrays(Utf8JsonWriter jsonWriter, JsonElement root1, JsonElement root2)
{
    Debug.Assert(root1.ValueKind == JsonValueKind.Array);
    Debug.Assert(root2.ValueKind == JsonValueKind.Array);

    jsonWriter.WriteStartArray();

    // Write all the elements from both JSON arrays
    foreach (JsonElement element in root1.EnumerateArray())
    {
        element.WriteTo(jsonWriter);
    }
    foreach (JsonElement element in root2.EnumerateArray())
    {
        element.WriteTo(jsonWriter);
    }

    jsonWriter.WriteEndArray();
}

注意:如果性能对您的场景至关重要,此方法(即使书写缩进)优于Newtonsoft.Json的Merge方法在运行时和分配方面。也就是说,可以根据需要使实现更快(例如,不要写缩进,缓存 outputBuffer,不要 accept/return 字符串等)。


BenchmarkDotNet=v0.12.0, OS=Windows 10.0.19041
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.100-alpha1-015914
  [Host]     : .NET Core 5.0.0 (CoreCLR 5.0.19.56303, CoreFX 5.0.19.56306), X64 RyuJIT
  Job-LACFYV : .NET Core 5.0.0 (CoreCLR 5.0.19.56303, CoreFX 5.0.19.56306), X64 RyuJIT

PowerPlanMode=00000000-0000-0000-0000-000000000000  

|          Method |     Mean |    Error |   StdDev |   Median |      Min |      Max | Ratio |  Gen 0 |  Gen 1 | Gen 2 | Allocated |
|---------------- |---------:|---------:|---------:|---------:|---------:|---------:|------:|-------:|-------:|------:|----------:|
| MergeNewtonsoft | 29.01 us | 0.570 us | 0.656 us | 28.84 us | 28.13 us | 30.19 us |  1.00 | 7.0801 | 0.0610 |     - |  28.98 KB |
|       Merge_New | 16.41 us | 0.293 us | 0.274 us | 16.41 us | 16.02 us | 17.00 us |  0.57 | 1.7090 |      - |     - |   6.99 KB |