69

I have this json

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

How do I get the 48e86841-f62c-42c9-ae20-b54ba8c35d6d out of it? All examples I can find show to do something like

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

But I don't have a type that fits this definition and I don't want to create one. I've tried deserializing to dynamic but I was unable to get that working.

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''

Can anyone give any suggestions?


edit:

I just figured out I can do this:

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

This does work - but is there a better way?

dbc
  • 104,963
  • 20
  • 228
  • 340
Nick
  • 4,556
  • 3
  • 29
  • 53

9 Answers9

54

you can deserialize to a Dictionary:

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

Or just deserialize to Object which will yield a JsonElement that you can call GetProperty on.

IronMan
  • 1,854
  • 10
  • 7
  • 10
    `Dictionary` ain't a bad option too. – benmccallum Jan 20 '20 at 16:05
  • How do you read value from dynamic object ? because I am getting an error `RuntimeBinderException: 'System.Text.Json.JsonElement' does not contain a definition for 'MyProperty'` – Muflix Jul 08 '20 at 15:05
  • Unfortunately, Dictionary will fail if the response Json contains string and non string types i.e. numbers. – Suneet Nangia Jul 27 '20 at 22:44
  • 2
    "Or just deserialize to Object which will yield a JsonElement that you can call GetProperty on." Is there an example you can point me to? – Macindows Sep 09 '20 at 16:24
  • 3
    using System.Text.Json; var json = JsonSerializer.Deserialize(json) as JsonElement?; var tmp = json?.GetProperty("id") – Jay Oct 08 '20 at 21:02
  • 4
    I think you can as well do this: var je_root = JsonSerializer.Deserialize(jsonstr); and then access it e.g. like so: int myvalue = je_root.GetProperty("MyProperty").GetProperty("MySubProperty").GetInt32(); – KarloX Nov 22 '20 at 07:22
  • 3
    @Muflix - if you need to test if a property exists, you can deserialise to a JsonElement and then use [TryGetProperty](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonelement.trygetproperty?view=net-5.0#System_Text_Json_JsonElement_TryGetProperty_System_String_System_Text_Json_JsonElement__): `JsonSerializer.Deserialize().TryGetProperty("SomeProp", out var element)` – Sean Kearon May 29 '21 at 06:20
29

Support for JsonObject has been added in .NET 6 using System.Text.Json.Nodes.

Example:

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

Sources:

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

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

Ogglas
  • 62,132
  • 37
  • 328
  • 418
  • @haldo Now I understand and then you are correct. Added working example code with `dynamic` – Ogglas Feb 07 '22 at 08:53
  • 1
    Thanks - I think that's better. I got very confused yesterday when I couldn't get the `dynamic` syntax working. Hopefully it'll prevent someone else being confused too! I'll tidy up my comments – haldo Feb 07 '22 at 11:09
17

I've recently migrated a project from ASP.NET Core 2.2 to 3, and I'm having this inconvenience. In our team we value lean dependencies, so we are trying to avoid including Newtonsoft.JSON back and try using System.Text.Json. We also decided not to use a ton of POCO objects just for JSON serialization, because our backend models are more complex than needed for Web APIs. Also, because of nontrivial behaviour encapsulation, the backend models cannot be easily used to serialize/deserialize JSON strings.

I understand that System.Text.Json is supposed to be faster than Newtonsoft.JSON, but I believe this has a lot to do with ser/deser from/to specific POCO classes. Anyway, speed was not on our list of pros/cons for this decision, so YMMV.

Long story short, for the time being I wrote a small dynamic object wrapper that unpacks the JsonElements from System.Text.Json and tries to convert/cast as best as possible. The typical usage is to read the request body as a dynamic object. Again, I'm pretty sure this approach kills any speed gains, but that was not a concern for our use case.

This is the class:

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

and this is an example usage, to parse the request body - one part is in a base class for all my WebAPI controllers, that exposes the body as a dynamic object:

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

and can be used in the actual controller like this:

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

If needed, you can integrate parsing for GUIDs or other specific data types as needed - while we all wait for some official / framework-sanctioned solution.

Alex Mazzariol
  • 2,456
  • 1
  • 19
  • 21
  • 2
    `JsonDocument` needs to be disposed when it goes out of scope, according to the [docs](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsondocument?view=netcore-3.1#remarks), to prevent a memory leak. When you need to use the `RootElement` outside the lifetime of the document, you must [clone](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonelement.clone?view=netcore-3.1#System_Text_Json_JsonElement_Clone) it. – dbc Aug 05 '20 at 15:56
9

Actual way to parse string in 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();
bobah75
  • 3,500
  • 1
  • 16
  • 25
1

You can use the following extension method to query data like "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;

Simple to use as follows

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"));
fullstackhero
  • 61
  • 1
  • 2
1

I wrote an extension method for this purpose. You can safely use as following:

var jsonElement = JsonSerializer.Deserialize<JsonElement>(json);
var guid = jsonElement.TryGetValue<Guid>("id");

This is the extension class.

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

Reason

The reason behind using this extension instead of JsonNode class is because if you need a Controller method accepts just an object without exposing it's model class Asp.Net Core model binding uses JsonElement struct to map the json string. At this point (as far as I know) there is no simple way to convert the JsonElement to JsonNode and when your object can be anything the JsonElement methods will throw exceptions for undefined fields while JsonNode don't.

[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();
}
iBener
  • 469
  • 6
  • 18
0

Solution that worked for me in .NET 6 Azure HTTPTrigger Function, without using class object

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

This is what I would use for quick testing purposes rather than clean POCO object.

Naveen Kumar V
  • 2,559
  • 2
  • 29
  • 43
-1

update to .NET Core 3.1 to support

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;
        }
    }
ruson
  • 95
  • 1
  • 1
  • 4
  • 1
    `JsonSerializer.Deserialize` doesn't help you to extract value from JsonValueKind and upgrading .Net Core 3.1 doesn't change the behaviour of System.Text.Json – Kuroro Apr 01 '20 at 06:19
  • @Kuroro You are not right, you can easily cast ExpandoObject to JsonElement and get everything – Mightywill Mar 17 '21 at 09:18
  • 2
    It works for the first level properties but for sublevels doesn't work. It cast to {System.Text.Json.JsonElement} – Rodrigo Perez Burgues Mar 17 '21 at 16:27
-1

You can also deserialize your json to an object of your target class, and then read its properties as per normal:

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

where DeSerializeFromStrToObj is a custom class that makes use of reflection to instantiate an object of a targeted class:

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

You can test your jsons for example here

Here you find a complete working example with different ways of serialization and deserialization that might be of interest for you and/or future readers:

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;
        }
    }
}
}
Paul Efford
  • 261
  • 4
  • 12