How to get value of object inside object using System.Text.Json in .net core 3.1

Darshana picture Darshana · Jan 18, 2020 · Viewed 11.9k times · Source

Created a .Net Core 3.1 Web Application and posted request where Requested model looks like,

public class RequestPayload
    {
        public string MessageName { get; set; }

        public object Payload { get; set; }
    }

I am very new to core 3.1 and struggling to get the value of Payload property, Can anyone help me on this?

While finding the solution I also compared Newtonsoft and System.Text.Json and got Error.

Using Newtonsoft I am able to Serialize and Deserialize a model shown below,

public class RequestPayload
    {
        public string MessageName { get; set; }

        public object Payload { get; set; }

        //Problem is here -> TYPE
        public Type PayloadType { get; set; }
    }

but using System.Text.Json I am not While serializing got error "System.Text.Json.JsonException: 'A possible object cycle was detected which is not supported."

To test deserialization, somehow created JSON and tries to deserialize it using System.Text.Json but getting an error "System.Text.Json.JsonException: 'The JSON value could not be converted to System.Type. "

Used System.Text.Json.JsonSerializer, is it an issue or is there any other possibility to make this works?

Answer

ahsonkhan picture ahsonkhan · Jan 18, 2020

I am very new to core 3.1 and struggling to get the value of Payload property, Can anyone help me on this?

For System.Object properties, unlike Newtonsoft.Json, System.Text.Json does not try to infer the type of the JSON payload for primitive values (such as true, 12345.67, "hello"). Similarly, for complex JSON values like objects and arrays (such as {"Name":"hi"} or [1, 2, 3]), the object property is set as a boxed JsonElement that represents the passed-in JSON. This is similar to how Newtonsoft.Json stores a JObject into the object property for complex types. See https://docs.microsoft.com/en-us/dotnet/api/system.text.json.jsonelement?view=netcore-3.1

Like how you would with Newtonsoft.Json's JObject, you can traverse and access values within the JSON Document Object Model (DOM) using the JsonElement and call conversion APIs on it to get .NET values (such as GetProperty(String) and GetInt32()).

The following example shows how you can access the Payload values, once you have deserialized the JSON into a RequestPayload.

private static void ObjectPropertyExample()
{
    using JsonDocument doc = JsonDocument.Parse("{\"Name\":\"Darshana\"}");
    JsonElement payload = doc.RootElement.Clone();

    var requestPayload = new RequestPayload
    {
        MessageName = "message",
        Payload = payload
    };

    string json = JsonSerializer.Serialize(requestPayload);
    Console.WriteLine(json);
    // {"MessageName":"message","Payload":{"Name":"Darshana"}}

    RequestPayload roundtrip = JsonSerializer.Deserialize<RequestPayload>(json);

    JsonElement element = (JsonElement)roundtrip.Payload;
    string name = element.GetProperty("Name").GetString();
    Assert.Equal("Darshana", name);
}

While finding the solution I also compared Newtonsoft and System.Text.Json and got Error.

Even though serializing a class that contains a System.Type property is OK to do, it is not recommended, especially for web applications (there are potential issues with information disclosure though).

On the other hand, deserialization JSON into a class that contains a Type property, especially using Type.GetType(untrusted-string-input) is definitely not recommended since it introduces potential security vulnerabilities in your application.

This is why the built-in System.Text.Json intentionally does not support serializing/deserializing Type properties. The exception message you are seeing while serializing is because Type contains a cycle within its object graph and the JsonSerializer doesn't currently handle cycles. If you only care about serializing (i.e. writing) the class into JSON, you could create your own JsonConverter<Type> to add support for it (to produce the same JSON that Newtonsoft.Json would). Something like the following will work:

private class CustomJsonConverterForType : JsonConverter<Type>
{
    public override Type Read(ref Utf8JsonReader reader, Type typeToConvert,
        JsonSerializerOptions options)
    {
        // Caution: Deserialization of type instances like this 
        // is not recommended and should be avoided
        // since it can lead to potential security issues.

        // If you really want this supported (for instance if the JSON input is trusted):
        // string assemblyQualifiedName = reader.GetString();
        // return Type.GetType(assemblyQualifiedName);
        throw new NotSupportedException();
    }

    public override void Write(Utf8JsonWriter writer, Type value,
        JsonSerializerOptions options)
    {
        // Use this with caution, since you are disclosing type information.
        writer.WriteStringValue(value.AssemblyQualifiedName);
    }
}

You can then add the custom converter into the options and pass that to JsonSerializer.Serialize:

var options = new JsonSerializerOptions();
options.Converters.Add(new CustomJsonConverterForType());

Consider re-evaluating why you need the Type property on your class that is being serialized and deserialized to begin with.

See https://github.com/dotnet/corefx/issues/42712 for more information and context around why you shouldn't deserialize classes containing Type properties using Type.GetType(string).

Here is more information on how to write a custom converter: https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to

An approach that can work more safely (and hence what I would recommend) is to use a type discriminator enum which contains the list of statically known types that you expect and support and explicitly create those types based on the enum values within the JsonConverter<Type>.

Here's an example of what that would look like:

// Let's assume these are the list of types we expect for the `Type` property
public class ExpectedType1 { }
public class ExpectedType2 { }
public class ExpectedType3 { }

public class CustomJsonConverterForType : JsonConverter<Type>
{
    public override Type Read(ref Utf8JsonReader reader, Type typeToConvert,
        JsonSerializerOptions options)
    {
        TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();

        Type type = typeDiscriminator switch
        {
            TypeDiscriminator.ExpectedType1 => typeof(ExpectedType1),
            TypeDiscriminator.ExpectedType2 => typeof(ExpectedType2),
            TypeDiscriminator.ExpectedType3 => typeof(ExpectedType3),
            _ => throw new NotSupportedException(),
        };
        return type;
    }

    public override void Write(Utf8JsonWriter writer, Type value,
        JsonSerializerOptions options)
    {
        if (value == typeof(ExpectedType1))
        {
            writer.WriteNumberValue((int)TypeDiscriminator.ExpectedType1);
        }
        else if (value == typeof(ExpectedType2))
        {
            writer.WriteNumberValue((int)TypeDiscriminator.ExpectedType2);
        }
        else if (value == typeof(ExpectedType3))
        {
            writer.WriteNumberValue((int)TypeDiscriminator.ExpectedType3);
        }
        else
        {
            throw new NotSupportedException();
        }
    }

    // Used to map supported types to an integer and vice versa.
    private enum TypeDiscriminator
    {
        ExpectedType1 = 1,
        ExpectedType2 = 2,
        ExpectedType3 = 3,
    }
}

private static void TypeConverterExample()
{
    var requestPayload = new RequestPayload
    {
        MessageName = "message",
        Payload = "payload",
        PayloadType = typeof(ExpectedType1)
    };

    var options = new JsonSerializerOptions()
    {
        Converters = { new CustomJsonConverterForType() }
    };

    string json = JsonSerializer.Serialize(requestPayload, options);
    Console.WriteLine(json);
    // {"MessageName":"message","Payload":"payload","PayloadType":1}

    RequestPayload roundtrip = JsonSerializer.Deserialize<RequestPayload>(json, options);
    Assert.Equal(typeof(ExpectedType1), roundtrip.PayloadType);
}