使用 System.Text.Json 获取嵌套属性
Getting nested properties with System.Text.Json
我在我的项目中使用 System.Text.Json
,因为我正在处理大文件,所以也决定用它来处理 GraphQL 响应。
由于 GraphQL 的性质,有时我会得到高度嵌套的响应,这些响应不固定并且没有意义映射到 class。我通常需要检查响应的一些属性。
我的问题是 JsonElement
。检查嵌套属性感觉很笨拙,我觉得应该有更好的方法来解决这个问题。
例如,使用下面的代码模拟我得到的响应。我只想检查是否存在 2 个属性(id 和 originalSrc)以及它们是否确实获得了它们的价值,但感觉就像我已经把代码吃了一顿。有没有 better/clearer/more 简洁的写法?
var raw = @"{
""data"": {
""products"": {
""edges"": [
{
""node"": {
""id"": ""gid://shopify/Product/4534543543316"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": ""gid://shopify/ProductImage/146345345339732""
}
}
}
]
}
}
}";
var doc = JsonSerializer.Deserialize<JsonElement>(raw);
JsonElement node = new JsonElement();
string productIdString = null;
if (doc.TryGetProperty("data", out var data))
if (data.TryGetProperty("products", out var products))
if (products.TryGetProperty("edges", out var edges))
if (edges.EnumerateArray().FirstOrDefault().ValueKind != JsonValueKind.Undefined && edges.EnumerateArray().First().TryGetProperty("node", out node))
if (node.TryGetProperty("id", out var productId))
productIdString = productId.GetString();
string originalSrcString = null;
if(node.ValueKind != JsonValueKind.Undefined && node.TryGetProperty("featuredImage", out var featuredImage))
if (featuredImage.TryGetProperty("originalSrc", out var originalSrc))
originalSrcString = originalSrc.GetString();
if (!string.IsNullOrEmpty(productIdString))
{
//do stuff
}
if (!string.IsNullOrEmpty(originalSrcString))
{
//do stuff
}
这不是大量的代码,但检查少数属性是如此常见,我想要一种更清晰、更易读的方法。
您可以添加几个扩展方法,通过 属性 名称或数组索引访问子 JsonElement
值,如果找不到则返回可为 null 的值:
public static partial class JsonExtensions
{
public static JsonElement? Get(this JsonElement element, string name) =>
element.ValueKind != JsonValueKind.Null && element.ValueKind != JsonValueKind.Undefined && element.TryGetProperty(name, out var value)
? value : (JsonElement?)null;
public static JsonElement? Get(this JsonElement element, int index)
{
if (element.ValueKind == JsonValueKind.Null || element.ValueKind == JsonValueKind.Undefined)
return null;
// Throw if index < 0
return index < element.GetArrayLength() ? element[index] : null;
}
}
现在可以使用 null 条件运算符将访问嵌套值的调用链接在一起 ?.
:
var doc = JsonSerializer.Deserialize<JsonElement>(raw);
var node = doc.Get("data")?.Get("products")?.Get("edges")?.Get(0)?.Get("node");
var productIdString = node?.Get("id")?.GetString();
var originalSrcString = node?.Get("featuredImage")?.Get("originalSrc")?.GetString();
Int64? someIntegerValue = node?.Get("Size")?.GetInt64(); // You could use "var" here also, I used Int64? to make the inferred type explicit.
备注:
如果传入元素不是预期类型(对象或数组或 null/missing),上述扩展方法将抛出异常。如果您不想在意外值类型上出现异常,则可以放松对 ValueKind
的检查。
有一个开放的 API 增强请求 Add JsonPath support to JsonDocument/JsonElement #31068. Querying via JSONPath,如果实现,将使这类事情变得更容易。
如果您从 Newtonsoft 移植代码,请注意 JObject
returns null
缺少 属性,而 JArray
抛出索引越界。因此,您可能希望在尝试模拟 Newtonsoft 的行为时直接使用 JElement
数组索引器,就像这样,因为它也会抛出索引越界:
var node = doc.Get("data")?.Get("products")?.Get("edges")?[0].Get("node");
演示 fiddle here.
为了使我的代码更具可读性,我创建了一个方法,该方法使用带 System.Text.Json 的点分隔路径,类似于 Newtonsoft.Json 中 SelectToken()
方法的路径参数。
JsonElement jsonElement = GetJsonElement(doc, "data.products.edges");
然后我使用 jsonElement.ValueKind
来检查 return 类型。
private static JsonElement GetJsonElement(JsonElement jsonElement, string path)
{
if (jsonElement.ValueKind == JsonValueKind.Null ||
jsonElement.ValueKind == JsonValueKind.Undefined)
{
return default;
}
string[] segments =
path.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
for (int n = 0; n < segments.Length; n++)
{
jsonElement = jsonElement.TryGetProperty(segments[n], out JsonElement value) ? value : default;
if (jsonElement.ValueKind == JsonValueKind.Null ||
jsonElement.ValueKind == JsonValueKind.Undefined)
{
return default;
}
}
return jsonElement;
}
我创建了另一个简单的方法来检索 returned JsonElement
的值作为字符串。
private static string GetJsonElementValue(JsonElement jsonElement)
{
return
jsonElement.ValueKind != JsonValueKind.Null &&
jsonElement.ValueKind != JsonValueKind.Undefined ?
jsonElement.ToString() :
default;
}
以下是应用于 OP 样本的两个函数:
public void Test()
{
string raw = @"{
""data"": {
""products"": {
""edges"": [
{
""node"": {
""id"": ""gid://shopify/Product/4534543543316"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": ""gid://shopify/ProductImage/146345345339732""
}
}
}
]
}
}
}";
JsonElement doc = JsonSerializer.Deserialize<JsonElement>(raw);
JsonElement jsonElementEdges = GetJsonElement(doc, "data.products.edges");
string originalSrcString = default;
string originalIdString = default;
if (jsonElementEdges.ValueKind == JsonValueKind.Array)
{
int index = 0; // Get the first element in the 'edges' array
JsonElement edgesFirstElem =
jsonElementEdges.EnumerateArray().ElementAtOrDefault(index);
JsonElement jsonElement =
GetJsonElement(edgesFirstElem, "node.featuredImage.originalSrc");
originalSrcString = GetJsonElementValue(jsonElement);
jsonElement =
GetJsonElement(edgesFirstElem, "node.featuredImage.id");
originalIdString = GetJsonElementValue(jsonElement);
}
if (!string.IsNullOrEmpty(originalSrcString))
{
// do stuff
}
if (!string.IsNullOrEmpty(originalIdString))
{
// do stuff
}
}
感谢 Dave B 的好主意。我对其进行了改进,使其在访问数组元素时更加高效,而无需编写太多代码。
string raw = @"{
""data"": {
""products"": {
""edges"": [
{
""node"": {
""id"": ""gid://shopify/Product/4534543543316"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": ""gid://shopify/ProductImage/146345345339732""
}
}
},
{
""node"": {
""id"": ""gid://shopify/Product/123456789"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": [
""gid://shopify/ProductImage/123456789"",
""gid://shopify/ProductImage/666666666""
]
},
""1"": {
""name"": ""Tuanh""
}
}
}
]
}
}
}";
用法也很简单
JsonElement doc = JsonSerializer.Deserialize<JsonElement>(raw);
JsonElement jsonElementEdges = doc.GetJsonElement("data.products.edges.1.node.1.name");
public static JsonElement GetJsonElement(this JsonElement jsonElement, string path)
{
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return default;
string[] segments = path.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries);
foreach (var segment in segments)
{
if (int.TryParse(segment, out var index) && jsonElement.ValueKind == JsonValueKind.Array)
{
jsonElement = jsonElement.EnumerateArray().ElementAtOrDefault(index);
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return default;
continue;
}
jsonElement = jsonElement.TryGetProperty(segment, out var value) ? value : default;
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return default;
}
return jsonElement;
}
public static string? GetJsonElementValue(this JsonElement jsonElement) => jsonElement.ValueKind != JsonValueKind.Null &&
jsonElement.ValueKind != JsonValueKind.Undefined
? jsonElement.ToString()
: default;
我开发了一个名为JsonEasyNavigation的小库,你可以在github or from nuget.org上获取它。它允许您使用类似索引器的语法在 JSON 域对象模型中导航:
var jsonDocument = JsonDocument.Parse(json);
var nav = jsonDocument.ToNavigation();
ToNavigation() 方法将 JsonDocument 转换为名为 JsonNavigationElement 的只读结构。它有 属性 和数组项索引器,例如:
var item = nav["data"]["product"]["edges"][0];
然后您可以像这样检查是否存在实际项目:
if (item.Exist)
{
var id = item["id"].GetStringOrEmpty();
// ...
}
希望你会觉得有用。
我在我的项目中使用 System.Text.Json
,因为我正在处理大文件,所以也决定用它来处理 GraphQL 响应。
由于 GraphQL 的性质,有时我会得到高度嵌套的响应,这些响应不固定并且没有意义映射到 class。我通常需要检查响应的一些属性。
我的问题是 JsonElement
。检查嵌套属性感觉很笨拙,我觉得应该有更好的方法来解决这个问题。
例如,使用下面的代码模拟我得到的响应。我只想检查是否存在 2 个属性(id 和 originalSrc)以及它们是否确实获得了它们的价值,但感觉就像我已经把代码吃了一顿。有没有 better/clearer/more 简洁的写法?
var raw = @"{
""data"": {
""products"": {
""edges"": [
{
""node"": {
""id"": ""gid://shopify/Product/4534543543316"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": ""gid://shopify/ProductImage/146345345339732""
}
}
}
]
}
}
}";
var doc = JsonSerializer.Deserialize<JsonElement>(raw);
JsonElement node = new JsonElement();
string productIdString = null;
if (doc.TryGetProperty("data", out var data))
if (data.TryGetProperty("products", out var products))
if (products.TryGetProperty("edges", out var edges))
if (edges.EnumerateArray().FirstOrDefault().ValueKind != JsonValueKind.Undefined && edges.EnumerateArray().First().TryGetProperty("node", out node))
if (node.TryGetProperty("id", out var productId))
productIdString = productId.GetString();
string originalSrcString = null;
if(node.ValueKind != JsonValueKind.Undefined && node.TryGetProperty("featuredImage", out var featuredImage))
if (featuredImage.TryGetProperty("originalSrc", out var originalSrc))
originalSrcString = originalSrc.GetString();
if (!string.IsNullOrEmpty(productIdString))
{
//do stuff
}
if (!string.IsNullOrEmpty(originalSrcString))
{
//do stuff
}
这不是大量的代码,但检查少数属性是如此常见,我想要一种更清晰、更易读的方法。
您可以添加几个扩展方法,通过 属性 名称或数组索引访问子 JsonElement
值,如果找不到则返回可为 null 的值:
public static partial class JsonExtensions
{
public static JsonElement? Get(this JsonElement element, string name) =>
element.ValueKind != JsonValueKind.Null && element.ValueKind != JsonValueKind.Undefined && element.TryGetProperty(name, out var value)
? value : (JsonElement?)null;
public static JsonElement? Get(this JsonElement element, int index)
{
if (element.ValueKind == JsonValueKind.Null || element.ValueKind == JsonValueKind.Undefined)
return null;
// Throw if index < 0
return index < element.GetArrayLength() ? element[index] : null;
}
}
现在可以使用 null 条件运算符将访问嵌套值的调用链接在一起 ?.
:
var doc = JsonSerializer.Deserialize<JsonElement>(raw);
var node = doc.Get("data")?.Get("products")?.Get("edges")?.Get(0)?.Get("node");
var productIdString = node?.Get("id")?.GetString();
var originalSrcString = node?.Get("featuredImage")?.Get("originalSrc")?.GetString();
Int64? someIntegerValue = node?.Get("Size")?.GetInt64(); // You could use "var" here also, I used Int64? to make the inferred type explicit.
备注:
如果传入元素不是预期类型(对象或数组或 null/missing),上述扩展方法将抛出异常。如果您不想在意外值类型上出现异常,则可以放松对
ValueKind
的检查。有一个开放的 API 增强请求 Add JsonPath support to JsonDocument/JsonElement #31068. Querying via JSONPath,如果实现,将使这类事情变得更容易。
如果您从 Newtonsoft 移植代码,请注意
JObject
returnsnull
缺少 属性,而JArray
抛出索引越界。因此,您可能希望在尝试模拟 Newtonsoft 的行为时直接使用JElement
数组索引器,就像这样,因为它也会抛出索引越界:var node = doc.Get("data")?.Get("products")?.Get("edges")?[0].Get("node");
演示 fiddle here.
为了使我的代码更具可读性,我创建了一个方法,该方法使用带 System.Text.Json 的点分隔路径,类似于 Newtonsoft.Json 中 SelectToken()
方法的路径参数。
JsonElement jsonElement = GetJsonElement(doc, "data.products.edges");
然后我使用 jsonElement.ValueKind
来检查 return 类型。
private static JsonElement GetJsonElement(JsonElement jsonElement, string path)
{
if (jsonElement.ValueKind == JsonValueKind.Null ||
jsonElement.ValueKind == JsonValueKind.Undefined)
{
return default;
}
string[] segments =
path.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
for (int n = 0; n < segments.Length; n++)
{
jsonElement = jsonElement.TryGetProperty(segments[n], out JsonElement value) ? value : default;
if (jsonElement.ValueKind == JsonValueKind.Null ||
jsonElement.ValueKind == JsonValueKind.Undefined)
{
return default;
}
}
return jsonElement;
}
我创建了另一个简单的方法来检索 returned JsonElement
的值作为字符串。
private static string GetJsonElementValue(JsonElement jsonElement)
{
return
jsonElement.ValueKind != JsonValueKind.Null &&
jsonElement.ValueKind != JsonValueKind.Undefined ?
jsonElement.ToString() :
default;
}
以下是应用于 OP 样本的两个函数:
public void Test()
{
string raw = @"{
""data"": {
""products"": {
""edges"": [
{
""node"": {
""id"": ""gid://shopify/Product/4534543543316"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": ""gid://shopify/ProductImage/146345345339732""
}
}
}
]
}
}
}";
JsonElement doc = JsonSerializer.Deserialize<JsonElement>(raw);
JsonElement jsonElementEdges = GetJsonElement(doc, "data.products.edges");
string originalSrcString = default;
string originalIdString = default;
if (jsonElementEdges.ValueKind == JsonValueKind.Array)
{
int index = 0; // Get the first element in the 'edges' array
JsonElement edgesFirstElem =
jsonElementEdges.EnumerateArray().ElementAtOrDefault(index);
JsonElement jsonElement =
GetJsonElement(edgesFirstElem, "node.featuredImage.originalSrc");
originalSrcString = GetJsonElementValue(jsonElement);
jsonElement =
GetJsonElement(edgesFirstElem, "node.featuredImage.id");
originalIdString = GetJsonElementValue(jsonElement);
}
if (!string.IsNullOrEmpty(originalSrcString))
{
// do stuff
}
if (!string.IsNullOrEmpty(originalIdString))
{
// do stuff
}
}
感谢 Dave B 的好主意。我对其进行了改进,使其在访问数组元素时更加高效,而无需编写太多代码。
string raw = @"{
""data"": {
""products"": {
""edges"": [
{
""node"": {
""id"": ""gid://shopify/Product/4534543543316"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": ""gid://shopify/ProductImage/146345345339732""
}
}
},
{
""node"": {
""id"": ""gid://shopify/Product/123456789"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": [
""gid://shopify/ProductImage/123456789"",
""gid://shopify/ProductImage/666666666""
]
},
""1"": {
""name"": ""Tuanh""
}
}
}
]
}
}
}";
用法也很简单
JsonElement doc = JsonSerializer.Deserialize<JsonElement>(raw);
JsonElement jsonElementEdges = doc.GetJsonElement("data.products.edges.1.node.1.name");
public static JsonElement GetJsonElement(this JsonElement jsonElement, string path)
{
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return default;
string[] segments = path.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries);
foreach (var segment in segments)
{
if (int.TryParse(segment, out var index) && jsonElement.ValueKind == JsonValueKind.Array)
{
jsonElement = jsonElement.EnumerateArray().ElementAtOrDefault(index);
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return default;
continue;
}
jsonElement = jsonElement.TryGetProperty(segment, out var value) ? value : default;
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return default;
}
return jsonElement;
}
public static string? GetJsonElementValue(this JsonElement jsonElement) => jsonElement.ValueKind != JsonValueKind.Null &&
jsonElement.ValueKind != JsonValueKind.Undefined
? jsonElement.ToString()
: default;
我开发了一个名为JsonEasyNavigation的小库,你可以在github or from nuget.org上获取它。它允许您使用类似索引器的语法在 JSON 域对象模型中导航:
var jsonDocument = JsonDocument.Parse(json);
var nav = jsonDocument.ToNavigation();
ToNavigation() 方法将 JsonDocument 转换为名为 JsonNavigationElement 的只读结构。它有 属性 和数组项索引器,例如:
var item = nav["data"]["product"]["edges"][0];
然后您可以像这样检查是否存在实际项目:
if (item.Exist)
{
var id = item["id"].GetStringOrEmpty();
// ...
}
希望你会觉得有用。