使用 System.Text.Json 修改 JSON 文件

Modifying a JSON file using System.Text.Json

我知道您可以使用 Newtonsoft 轻松做到这一点。然而,当我使用 .NET Core 3.0 时,我正在尝试使用新方法与 JSON 文件交互——即 System.Text.Json——我拒绝相信我正在尝试做的事情有那么难!

我的应用程序需要列出尚未添加到我的数据库中的用户。为了获得所有用户的完整列表,该应用程序从网络 API 检索 JSON 字符串。我现在需要循环浏览每个用户并检查他们是否已添加到我的应用程序,然后再将新的 JSON 列表返回到我的视图,以便它可以向最终用户显示新的潜在用户。

因为我最终会在流程结束时返回另一个 JSON,所以我不想特别麻烦将其反序列化为模型。请注意,来自 API 的数据结构可能 会发生变化,但它 总是 有一个键,我可以从中比较我的数据库记录。

我的代码目前是这样的:

using (WebClient wc = new WebClient())
{
    var rawJsonDownload = wc.DownloadString("WEB API CALL");
    var users =  JsonSerializer.Deserialize<List<UserObject>>(rawJsonDownload);

    foreach (var user in users.ToList())
    {
        //Check if User is new
        if (CHECKS)
        {
            users.Remove(user);
        }
    }

    return Json(users); 
}

这似乎是一个 很多 的障碍,需要跳过才能实现一些对于 Newtonsoft 来说相当微不足道的事情。

任何人都可以建议我更好的方法来做到这一点——理想情况下,不需要 UserObject

您的问题是您想要检索、过滤和传递一些 JSON 而无需为 JSON 定义完整的数据模型。使用 Json.NET,您可以使用 LINQ to JSON 来达到这个目的。您的问题是,目前可以使用 System.Text.Json 轻松解决这个问题吗?

从 .NET 6 开始,使用 System.Text.Json 无法轻松完成此操作,因为它不支持 JSONPath which is often quite convenient in such applications. There is currently an open issue Add JsonPath support to JsonDocument/JsonElement #41537 跟踪。

话虽这么说,假设您有以下 JSON:

[
  {
    "id": 1,
    "name": "name 1",
    "address": {
      "Line1": "line 1",
      "Line2": "line 2"
    },
    // More properties omitted
  }
  //, Other array entries omitted
]

和一些 Predicate<long> shouldSkip 过滤器方法,指示是否不应返回具有特定 id 的条目,对应于您问题中的 CHECKS。你有什么选择?

在 .NET 6 及更高版本中,您可以将 JSON 解析为 JsonNode, edit its contents, and return the modified JSON. A JsonNode represents an editable JSON Document Object Model and thus most closely corresponds to Newtonsoft's JToken 层次结构。

以下代码显示了一个示例:

var root = JsonNode.Parse(rawJsonDownload).AsArray(); // AsArray() throws if the root node is not an array.
for (int i = root.Count - 1; i >= 0; i--)
{
    if (shouldSkip(root[i].AsObject()["id"].GetValue<long>()))
        root.RemoveAt(i);
}

return Json(root);

模型 fiddle #1 here

在 .NET Core 3.x 及更高版本中,您可以解析为 JsonDocument and return some filtered set of JsonElement 个节点 。如果过滤逻辑非常简单并且您不需要以任何其他方式修改 JSON,则此方法效果很好。但请注意 JsonDocument 的以下限制:

  • JsonDocumentJsonElement 是只读的。它们只能用于 检查 JSON 值,不能修改或创建 JSON 值。

  • JsonDocument 是一次性的,实际上必须进行处理以在高使用率场景下最大限度地减少垃圾收集器 (GC) 的影响,照着docs. In order to return a JsonElement you must clone吧。

题中的过滤场景很简单,可以用下面的代码:

using var usersDocument = JsonDocument.Parse(rawJsonDownload);
var users = usersDocument.RootElement.EnumerateArray()
    .Where(e => !shouldSkip(e.GetProperty("id").GetInt64()))
    .Select(e => e.Clone())
    .ToList();

return Json(users);

模型 fiddle #2 here.

在任何版本中,您都可以创建一个 部分 数据模型,它仅反序列化过滤所需的属性,其余的 JSON 绑定到 [JsonExtensionDataAttribute] 属性. 这应该允许您实现必要的过滤,而无需对整个数据模型进行硬编码。

为此,定义以下模型:

public class UserObject
{
    [JsonPropertyName("id")]
    public long Id { get; set; }
    
    [System.Text.Json.Serialization.JsonExtensionDataAttribute]
    public IDictionary<string, object> ExtensionData { get; set; }
}

并反序列化过滤如下:

var users = JsonSerializer.Deserialize<List<UserObject>>(rawJsonDownload);
users.RemoveAll(u => shouldSkip(u.Id));

return Json(users);

这种方法确保可以适当地反序列化与过滤相关的属性,而无需对 JSON 的其余部分做出任何假设。虽然这不像使用 LINQ to JSON 那样简单,但总的代码复杂性受限于过滤检查的复杂性,而不是 JSON 的复杂性。事实上,我认为这种方法在实践中比 JsonDocument 方法更容易使用,因为如果以后需要,它可以更容易地向 JSON 注入修改。

模型 fiddle #3 here.

无论您选择哪个,您都可以考虑放弃 WebClient 而使用 HttpClient 并使用 async 反序列化。例如:

var httpClient = new HttpClient(); // Cache statically and reuse in production
var root = await httpClient.GetFromJsonAsync<JsonArray>("WEB API CALL");

using var usersDocument = await JsonDocument.ParseAsync(await httpClient.GetStreamAsync("WEB API CALL"));

var users = await JsonSerializer.DeserializeAsync<List<UserObject>>(await httpClient.GetStreamAsync("WEB API CALL"));

您需要 您的 API 方法也是 async