简体   繁体   English

System.Text.Json 合并两个对象

[英]System.Text.Json Merge two objects

Is it possible to merge two json objects like this with System.Text.Json?是否可以将两个像这样的 json 对象与System.Text.Json?

Object 1 Object 1

{
   id: 1
   william: "shakespeare"
}

Object 2 Object 2

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

Result Object结果 Object

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

I am able to achieve it with newtonsoft.json like this我可以像这样使用 newtonsoft.json 来实现它

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

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

But is there a way with System.Text.Json ?但是System.Text.Json有没有办法?

There already exists an issue to request this feature for System.Text.Json : https://github.com/dotnet/corefx/issues/42466已经存在为System.Text.Json请求此功能的问题: https://github.com/dotnet/corefx/issues/42466

In the meantime, you could write your own Merge method based on the Utf8JsonWriter as a workaround (since the existing JsonDocument , JsonElement APIs are read-only).同时,您可以基于Utf8JsonWriter编写自己的Merge方法作为解决方法(因为现有的JsonDocumentJsonElement API 是只读的)。

If your JSON objects only contain non-null simple/primitive values and the order in which the properties show up isn't particularly concerning, the following, relatively straightforward, code sample should work for you:如果您的 JSON 对象仅包含非空简单/原始值并且属性显示的顺序不是特别重要,那么以下相对简单的代码示例应该适合您:

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 has different null handling when doing a merge where null doesn't override the value of the non-null property (when there are duplicates). Newtonsoft.Json在执行合并时具有不同的null处理,其中null不会覆盖非空属性的值(当有重复项时)。 I am not sure if you want that behavior or not.我不确定你是否想要这种行为。 If that's needed, you would need to modify the above method to handle the null cases.如果需要,您需要修改上述方法来处理null案例。 Here are the modifications:以下是修改:

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);
}

If your JSON objects can potentially contain nested JSON values including other objects and arrays , you would want to extend the logic to handle that too.如果您的 JSON 对象可能包含嵌套的 JSON 值,包括其他对象和 arrays ,那么您也需要扩展逻辑来处理它。 Something like this should work:像这样的东西应该工作:

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();
}

Note: If performance is critical for your scenario, this method (even with writing indented) out-performs the Newtonsoft.Json's Merge method both in terms of runtime and allocations.注意:如果性能对您的方案至关重要,则此方法(即使编写缩进)在运行时和分配方面都优于 Newtonsoft.Json 的Merge方法。 That said, the implementation could be made faster depending on need (for instance, don't write indented, cache the outputBuffer , don't accept/return strings, etc.).也就是说,可以根据需要加快实现速度(例如,不写缩进,缓存outputBuffer ,不接受/返回字符串等)。


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 |

As of.Net Core 3.0 merging of JSON objects is not implemented by System.Text.Json :截至.Net Core 3.0 JSON 对象的合并不是由System.Text.Json实现的:

More generally, JsonDocument is read-only .更一般地说, JsonDocument只读的 It

Provides a mechanism for examining the structural content of a JSON value without automatically instantiating data values.提供一种机制来检查JSON 值的结构内容,而无需自动实例化数据值。

As such it isn't designed to support modifying a JSON value in any way including merging another JSON value into it.因此,它不支持以任何方式修改 JSON 值,包括将另一个 JSON 值合并到其中。

There is currently an enhancement request to implement a modifiable JSON Document Object Model: Issue #39922: Writable Json DOM . There is currently an enhancement request to implement a modifiable JSON Document Object Model: Issue #39922: Writable Json DOM . It has an associated specification Writable JSON Document Object Model (DOM) for System.Text.Json .它有一个关联的规范Writable JSON 文档 Object Model (DOM) for System.Text.Json If this enhancement were implemented, merging of JSON documents would become possible.如果实施此增强功能,则可以合并 JSON 文档。 You could add an issue requesting functionality equivalent to JContainer.Merge() , linking back to Issue #39922 as a prerequisite.您可以添加与JContainer.Merge()等效的问题请求功能,并将链接回问题 #39922 作为先决条件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM