使用System.Text.Json如何从JSON中读取简单值?

70

我有这个 JSON

{"id":"48e86841-f62c-42c9-ae20-b54ba8c35d6d"}

我该如何从中提取出48e86841-f62c-42c9-ae20-b54ba8c35d6d?我能找到的所有示例都显示要执行类似以下操作:

TBD
var o = System.Text.Json.JsonSerializer.Deserialize<some-type>(json);
o.id // <- here's the ID!

但是我没有符合这个定义的类型,也不想创建一个。我尝试过将其反序列化为动态类型,但我无法使其工作。

var result = System.Text.Json.JsonSerializer.Deserialize<dynamic>(json);
result.id // <-- An exception of type 'Microsoft.CSharp.RuntimeBinder.RuntimeBinderException' occurred in System.Linq.Expressions.dll but was not handled in user code: ''System.Text.Json.JsonElement' does not contain a definition for 'id''

有人能给一些建议吗?


编辑:

我刚刚发现我可以这样做:

Guid id = System.Text.Json.JsonDocument.Parse(json).RootElement.GetProperty("id").GetGuid();

这个确实可以用 - 但是否有更好的方法呢?


你可以使用第三方库,比如Newtonsoft的Json吗? - andresantacruz
3
不,我们放弃了Newtonsoft因为它太慢了。 - Nick
我不认为我添加的内容是一个答案。 - Nick
9个回答

57

您可以反序列化为 Dictionary

var dict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(json)

或者只需反序列化为Object,这将产生一个JsonElement,您可以在其上调用GetProperty


10
Dictionary<string, object> 也不是一个坏的选择。 - benmccallum
2
或者只需反序列化为Object,这将产生一个JsonElement,您可以在其上调用GetProperty。您能指向一个例子吗? - Macindows
3
使用 System.Text.Json; var json = JsonSerializer.Deserialize<object>(json) as JsonElement?; var tmp = json?.GetProperty("id") - Jay
4
我认为你可以这样做: var je_root = JsonSerializer.Deserialize<JasonElement>(jsonstr); 然后可以通过以下方式访问: int myvalue = je_root.GetProperty("MyProperty").GetProperty("MySubProperty").GetInt32(); - KarloX
4
如果您需要测试一个属性是否存在,可以将其反序列化为JsonElement,然后使用TryGetProperty方法进行检索:JsonSerializer.Deserialize<JsonElement>().TryGetProperty("SomeProp", out var element)。请注意,此操作不应改变原始意义。 - Sean Kearon
显示剩余2条评论

29

.NET 6 中,使用 System.Text.Json.Nodes 添加了对 JsonObject 的支持。

示例:

const string Json = "{\"MyNumber\":42, \"MyArray\":[10,11]}";
// dynamic
{
    dynamic obj = JsonNode.Parse(Json);
    int number = (int)obj["MyNumber"];
    Debug.Assert(number == 42);

    obj["MyString"] = "Hello";
    Debug.Assert((string)obj["MyString"] == "Hello");
}

// JsonObject 
{
    JsonObject obj = JsonNode.Parse(Json).AsObject();
    int number = (int)obj["MyNumber"];
    Debug.Assert(number == 42);
    
    obj["MyString"] = "Hello";
    Debug.Assert((string)obj["MyString"] == "Hello");
}

资料来源:

https://github.com/dotnet/runtime/issues/53195

https://github.com/dotnet/runtime/issues/45188


@haldo 现在我明白了,你是正确的。添加了带有“dynamic”的工作示例代码。 - Ogglas
1
谢谢 - 我认为这样更好。昨天当我无法让“dynamic”语法工作时,我非常困惑。希望这能防止其他人感到困惑!我会整理我的注释。 - haldo

17

我最近将一个项目从ASP.NET Core 2.2迁移到3,遇到了这个问题。我们团队重视依赖的精简,因此我们试图避免重新包含Newtonsoft.JSON并尝试使用System.Text.Json。我们还决定不使用大量POCO对象仅用于JSON序列化,因为我们的后端模型比Web API所需的更复杂。另外,由于非平凡的行为封装,后端模型不能轻松地用于序列化/反序列化JSON字符串。

我理解System.Text.Json应该比Newtonsoft.JSON更快,但我认为这与从特定POCO类中进行序列化/反序列化有很大关系。无论如何,速度并不是我们做出这个决定的利弊清单上的考虑因素,所以YMMV。

长话短说,暂时我编写了一个小型动态对象包装器,从System.Text.Json中解压缩JsonElement,并尽可能地转换/强制类型转换。典型的用法是将请求主体读取为动态对象。再次强调,我相当确定这种方法会杀死任何速度增益,但这不是我们用例的关注点。

这是类:

    public class ReflectionDynamicObject : DynamicObject {
        public JsonElement RealObject { get; set; }

        public override bool TryGetMember (GetMemberBinder binder, out object result) {
            // Get the property value
            var srcData = RealObject.GetProperty (binder.Name);

            result = null;

            switch (srcData.ValueKind) {
                case JsonValueKind.Null:
                    result = null;
                    break;
                case JsonValueKind.Number:
                    result = srcData.GetDouble ();
                    break;
                case JsonValueKind.False:
                    result = false;
                    break;
                case JsonValueKind.True:
                    result = true;
                    break;
                case JsonValueKind.Undefined:
                    result = null;
                    break;
                case JsonValueKind.String:
                    result = srcData.GetString ();
                    break;
                case JsonValueKind.Object:
                    result = new ReflectionDynamicObject {
                        RealObject = srcData
                    };
                    break;
                case JsonValueKind.Array:
                    result = srcData.EnumerateArray ()
                        .Select (o => new ReflectionDynamicObject { RealObject = o })
                        .ToArray ();
                    break;
            }

            // Always return true; other exceptions may have already been thrown if needed
            return true;
        }
    }

这是一个使用示例,用于解析请求体 - 其中一部分位于我所有WebAPI控制器的基类中,该基类将请求体公开为动态对象:

    [ApiController]
    public class WebControllerBase : Controller {

        // Other stuff - omitted

        protected async Task<dynamic> JsonBody () {
            var result = await JsonDocument.ParseAsync (Request.Body);
            return new ReflectionDynamicObject {
                RealObject = result.RootElement
            };
        }
    }

并且可以像这样在实际控制器中使用:

//[...]
    [HttpPost ("")]
    public async Task<ActionResult> Post () {
        var body = await JsonBody ();
        var name = (string) body.Name;
        //[...]
    }
//[...]

如有需要,您可以根据需要集成GUID或其他特定数据类型的解析 - 同时我们都在等待一些官方/框架认可的解决方案。


2
根据文档,当JsonDocument超出范围时,需要将其处理掉以防止内存泄漏。如果您需要在文档的生命周期之外使用RootElement,则必须克隆它。 - dbc

9

在 System.Text.Json (.NET Core 3+) 中解析字符串的实际方法

        var jsonStr = "{\"id\":\"48e86841-f62c-42c9-ae20-b54ba8c35d6d\"}";
        using var doc = JsonDocument.Parse(jsonStr);
        var root = doc.RootElement;
        var id = root.GetProperty("id").GetGuid();

2
我为此编写了一个扩展方法。您可以安全地按照以下方式使用:
var jsonElement = JsonSerializer.Deserialize<JsonElement>(json);
var guid = jsonElement.TryGetValue<Guid>("id");

这是扩展类。
public static class JsonElementExtensions
{
    private static readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true };

    public static T? TryGetValue<T>(this JsonElement element, string propertyName)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            return default;
        }

        element.TryGetProperty(propertyName, out JsonElement property);

        if (property.ValueKind == JsonValueKind.Undefined ||
            property.ValueKind == JsonValueKind.Null)
        {
            return default;
        }

        try
        {
            return property.Deserialize<T>(options);
        }
        catch (JsonException)
        {
            return default;
        }
    }
}

原因

使用此扩展而不是JsonNode类的原因是,如果您需要一个Controller方法仅接受一个object而不暴露其模型类,则Asp.Net Core模型绑定使用JsonElement结构来映射json字符串。在此时(据我所知),没有简单的方法将JsonElement转换为JsonNode,并且当您的对象可以是任何内容时,JsonElement方法将对未定义字段抛出异常,而JsonNode则不会。

[HttpPost]
public IActionResult Post(object setupObject)
{
    var setup = (JsonElement)setupObject;
    var id = setup.TryGetValue<Guid>("id");
    var user = setup.TryGetValue<User?>("user");
    var account = setup.TryGetValue<Account?>("account");
    var payments = setup.TryGetValue<IEnumerable<Payments>?>("payments");

    // ...

    return Ok();
}

1
您可以使用以下扩展方法来查询数据,类似于“xpath”。
public static string? JsonQueryXPath(this string value, string xpath, JsonSerializerOptions? options = null) => value.Deserialize<JsonElement>(options).GetJsonElement(xpath).GetJsonElementValue();

        
        public static JsonElement GetJsonElement(this JsonElement jsonElement, string xpath)
        {
            if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
                return default;

            string[] segments = xpath.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;

简单易用,如下所示:
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""
                        }
                    }
                }
            ]
        }
        }
    }";

            System.Console.WriteLine(raw2.QueryJsonXPath("data.products.edges.0.node.featuredImage.id"));

0
在.NET 6 Azure HTTPTrigger Function中,我找到了一个解决方案,而不需要使用类对象。
    [Function("HTTPTrigger1")]
    public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
    {
        // Input: { "name": "Azure", "id": 123 }
        var reqBody = new StreamReader(req.Body).ReadToEnd();
        JsonObject obj = JsonNode.Parse(reqBody).AsObject();
        obj.TryGetPropertyValue ("name", out JsonNode jsnode);
        string name2 = jsnode.GetValue<string>();
        obj.TryGetPropertyValue ("id", out jsnode);
        int id2 = jsnode.GetValue<int>();

        // OR
        // using dictionary
        var data = JsonSerializer.Deserialize<Dictionary<string, object>> (reqBody);
        string name = data["name"].ToString () ?? "Anonymous";
        int.TryParse(data["id"].ToString(), out int id);

        _logger.LogInformation($"Hi {name}{id} {name2}{id2}. C# HTTP trigger function processed a request.");
        var response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
        response.WriteString("Welcome to Azure Functions!");
        return response;
    }

这是我用来进行快速测试而不是使用干净的POCO对象的内容。

-1

你也可以将你的 JSON 反序列化为目标类的对象,然后像平常一样读取它的属性:

var obj = DeSerializeFromStrToObj<ClassToSerialize>(jsonStr);
Console.WriteLine($"Property: {obj.Property}");

在这里,DeSerializeFromStrToObj 是一个自定义类,利用反射实例化目标类的对象:

    public static T DeSerializeFromStrToObj<T>(string json)
    {
        try
        {
            var o = (T)Activator.CreateInstance(typeof(T));

            try
            {
                var jsonDict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);

                var props = o.GetType().GetProperties();

                if (props == null || props.Length == 0)
                {
                    Debug.WriteLine($"Error: properties from target class '{typeof(T)}' could not be read using reflection");
                    return default;
                }

                if (jsonDict.Count != props.Length)
                {
                    Debug.WriteLine($"Error: number of json lines ({jsonDict.Count}) should be the same as number of properties ({props.Length})of our class '{typeof(T)}'");
                    return default;
                }

                foreach (var prop in props)
                {
                    if (prop == null)
                    {
                        Debug.WriteLine($"Error: there was a prop='null' in our target class '{typeof(T)}'");
                        return default;
                    }

                    if (!jsonDict.ContainsKey(prop.Name))
                    {
                        Debug.WriteLine($"Error: jsonStr does not refer to target class '{typeof(T)}'");
                        return default;
                    }

                    var value = jsonDict[prop.Name];
                    Type t = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
                    object safeValue = value ?? Convert.ChangeType(value, t);
                    prop.SetValue(o, safeValue, null); // initialize property
                }
                return o;
            }
            catch (Exception e2)
            {
                Debug.WriteLine(e2.Message);
                return o;
            }
        }
        catch (Exception e)
        {
            Debug.WriteLine(e.Message);
            return default;
        }
    }

你可以测试你的 JSON,例如 这里

在这里,你可以找到一个完整的工作示例,其中包含不同的序列化和反序列化方式,这可能对你和/或未来的读者有所帮助:

using System;
using System.Collections.Generic;
using System.Text.Json;
using static Json_Tests.JsonHelpers;

namespace Json_Tests
{

public class Class1
{
    public void Test()
    {
        var obj1 = new ClassToSerialize();
        var jsonStr = obj1.ToString();

        // if you have the class structure for the jsonStr (for example, if you have created the jsonStr yourself from your code):
        var obj2 = DeSerializeFromStrToObj<ClassToSerialize>(jsonStr);
        Console.WriteLine($"{nameof(obj2.Name)}: {obj2.Name}");

        // if you do not have the class structure for the jsonStr (forexample, jsonStr comes from a 3rd party service like the web):
        var obj3 = JsonSerializer.Deserialize<object>(jsonStr) as JsonElement?;
        var propName = nameof(obj1.Name);
        var propVal1 = obj3?.GetProperty("Name");// error prone
        Console.WriteLine($"{propName}: {propVal1}");
        JsonElement propVal2 = default;
        obj3?.TryGetProperty("Name", out propVal2);// error prone
        Console.WriteLine($"{propName}: {propVal2}");

        var obj4 = DeSerializeFromStrToDict(jsonStr);
        foreach (var pair in obj4)
            Console.WriteLine($"{pair.Key}: {pair.Value}");
    }
}

[Serializable]
public class ClassToSerialize
{
    // important: properties must have at least getters
    public string Name { get; } = "Paul";
    public string Surname{ get; set; } = "Efford";

    public override string ToString() => JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
}

public static class JsonHelpers
{
    /// <summary>
    /// to use if you do not have the class structure for the jsonStr (forexample, jsonStr comes from a 3rd party service like the web)
    /// </summary>
    public static Dictionary<string, string> DeSerializeFromStrToDict(string json)
    {
        try
        {
            return JsonSerializer.Deserialize<Dictionary<string, string>>(json);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
            return new Dictionary<string, string>(); // return empty
        }
    }

    /// <summary>
    /// to use if you have the class structure for the jsonStr (for example, if you have created the jsonStr yourself from your code)
    /// </summary>
    public static T DeSerializeFromStrToObj<T>(string json) // see this: https://json2csharp.com/#
    {
        try
        {
            var o = (T)Activator.CreateInstance(typeof(T));

            try
            {
                var jsonDict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);

                var props = o.GetType().GetProperties();

                if (props == null || props.Length == 0)
                {
                    Console.WriteLine($"Error: properties from target class '{typeof(T)}' could not be read using reflection");
                    return default;
                }

                if (jsonDict.Count != props.Length)
                {
                    Console.WriteLine($"Error: number of json lines ({jsonDict.Count}) should be the same as number of properties ({props.Length})of our class '{typeof(T)}'");
                    return default;
                }

                foreach (var prop in props)
                {
                    if (prop == null)
                    {
                        Console.WriteLine($"Error: there was a prop='null' in our target class '{typeof(T)}'");
                        return default;
                    }

                    if (!jsonDict.ContainsKey(prop.Name))
                    {
                        Console.WriteLine($"Error: jsonStr does not refer to target class '{typeof(T)}'");
                        return default;
                    }

                    var value = jsonDict[prop.Name];
                    Type t = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
                    object safeValue = value ?? Convert.ChangeType(value, t);
                    prop.SetValue(o, safeValue, null); // initialize property
                }
                return o;
            }
            catch (Exception e2)
            {
                Console.WriteLine(e2.Message);
                return o;
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
            return default;
        }
    }
}
}

-1

更新到 .NET Core 3.1 以进行支持

public static dynamic FromJson(this string json, JsonSerializerOptions options = null)
    {
        if (string.IsNullOrEmpty(json))
            return null;

        try
        {
            return JsonSerializer.Deserialize<ExpandoObject>(json, options);
        }
        catch
        {
            return null;
        }
    }

1
JsonSerializer.Deserialize<ExpandoObject> 无法帮助您从 JsonValueKind 中提取值,升级 .Net Core 3.1 也不会改变 System.Text.Json 的行为。 - Kuroro
@Kuroro,你说的不对,你可以轻松地将ExpandoObject转换为JsonElement并获取所有内容。 - Mightywill
2
它适用于第一级属性,但对于子级不起作用。它转换为{System.Text.Json.JsonElement}。 - Rodrigo Perez Burgues

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接