【问题标题】:System.Text.Json Merge two objectsSystem.Text.Json 合并两个对象
【发布时间】:2020-02-29 20:27:57
【问题描述】:

是否可以将两个这样的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有办法吗?

【问题讨论】:

  • 你为什么不想使用匿名类型。您可以轻松实现 JObject.Parse(new {propertyOne = One, propertyTwo = Two.. })
  • 这似乎在System.Text.Json 中不可用.Net Core 3.0。 1) JsonDocument 没有 MergePopulate 方法。 2) JsonSerializer 没有 MergePopulate 方法。
  • 事实上JsonDocument 目前是只读的,所以不支持合并。请参阅:Writable Json DOM #39922github.com/dotnet/corefx/blob/master/src/System.Text.Json/docs/…
  • @dbc 谢谢,看起来它可能会在未来出现。与此同时,我将继续在这部分应用程序中使用 newtonsoft。
  • 我应该把这个作为答案吗?

标签: c# .net-core json.net system.text.json


【解决方案1】:

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

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

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

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,不接受/返回字符串等)。


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 |

【讨论】:

【解决方案2】:

从 .Net Core 3.0 开始System.Text.Json 未实现 JSON 对象的合并:

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

提供一种机制来检查 JSON 值的结构内容,而无需自动实例化数据值。

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

目前有一个增强请求来实现可修改的 JSON 文档对象模型: Issue #39922: Writable Json DOM。它有一个相关的规范Writable JSON Document Object Model (DOM) for System.Text.Json。如果实施此增强功能,JSON 文档的合并将成为可能。您可以添加一个与 JContainer.Merge() 等效的问题请求功能,并作为先决条件链接回问题 #39922。

【讨论】:

    猜你喜欢
    • 2012-04-25
    • 2017-03-30
    • 2018-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-21
    • 2013-04-08
    • 2021-03-08
    相关资源
    最近更新 更多