Home > Software engineering >  In .NET Core 6 how replace Newtonsoft with System.Text.Json when creating dynamic object from nested
In .NET Core 6 how replace Newtonsoft with System.Text.Json when creating dynamic object from nested

Time:10-28

In C# I'm converting a method that returns a dynamic object from .NET Framework to Core 6. The method takes the json output from MediaInfo from any type of video or audio file. We were using JavaScriptSerializer but that's not available in .Net Core.

Here is an example of the json for a video file.

{"media": {"@ref": "d:\inetpub\ftproot\wbt\clicksafety\courses\references\clip_38_cvt.mp4","track": [{"@type": "general","videocount": "1","audiocount": "1","fileextension": "mp4","format": "mpeg-4","format_profile": "base media","codecid": "isom","codecid_compatible": "isom/iso2/avc1/mp41","vidSize": "6351654","vidSeconds": "48.048","overallbitrate": "1057551","vidFrameRate": "29.970","framecount": "1440","streamsize": "27456","headersize": "27448","datasize": "6324206","footersize": "0","isstreamable": "yes","file_created_date": "utc 2022-10-27 13:54:05.474","file_created_date_local": "2022-10-27 09:54:05.474","file_modified_date": "utc 2022-10-27 13:54:05.477","file_modified_date_local": "2022-10-27 09:54:05.477","encoded_application": "lavf58.45.100"},{"@type": "video","streamorder": "0","id": "1","format": "avc","format_profile": "high","format_level": "3.1","f
ormat_settings_cabac": "yes","format_settings_refframes": "4","codecid": "avc1","vidSeconds": "48.048","bitrate": "1000000","vidWidth": "1024","vidHeight": "576","sampled_vidWidth": "1024","sampled_vidHeight": "576","pixelaspectratio": "1.000","vidAspect": "1.778","vidRotation": "0.000","vidFrameRate_mode": "cfr","vidFrameRate_mode_original": "vfr","vidFrameRate": "29.970","framecount": "1440","colorspace": "yuv","chromasubsampling": "4:2:0","bitdepth": "8","scantype": "progressive","streamsize": "5991992","encoded_library": "x264 - core 161","encoded_library_name": "x264","encoded_library_version": "core 161","encoded_library_settings": "cabac=1 / ref=3 / deblock=1:0:0 / analyse=0x3:0x113 / me=hex / subme=7 / psy=1 / psy_rd=1.00:0.00 / mixed_ref=1 / me_range=16 / chroma_me=1 / trellis=1 / 8x8dct=1 / cqm=0 / deadzone=21,11 / fast_pskip=1 / chroma_qp_offset=-2 / threads=3 / lookahead_threads=1 / sliced_threads=0 / 
nr=0 / decimate=1 / interlaced=0 / bluray_compat=0 / constrained_intra=0 / bframes=3 / b_pyramid=2 / b_adapt=1 / b_bias=0 / direct=1 / weightb=1 / open_gop=0 / weightp=2 / keyint=250 / keyint_min=25 / scenecut=40 / intra_refresh=0 / rc_lookahead=40 / rc=abr / mbtree=1 / bitrate=1000 / ratetol=1.0 / qcomp=0.60 / qpmin=0 / qpmax=69 / qpstep=4 / ip_ratio=1.41 / aq=1:1.00","extra": {"codecconfigurationbox": "avcc"}},{"@type": "audio","streamorder": "1","id": "2","format": "aac","format_settings_sbr": "no (explicit)","format_additionalfeatures": "lc","codecid": "mp4a-40-2","vidSeconds": "47.416","vidSeconds_lastframe": "-0.046","bitrate_mode": "cbr","bitrate": "56050","channels": "2","channelpositions": "front: l r","channellayout": "l r","samplesperframe": "1024","vidAudio": "11025","samplingcount": "522761","vidFrameRate": "10.767","framecount": "511","compression_mode": "lossy","streamsize": "332206","streamsize_proportion": "0.05230","default": "yes","alternategroup": "1"}]}}

MediaInfo's json has a root and two or three children. However, the children's properties are always different in number and name from file to file depending on the file type and what created the audio or video. So, no... POCOs don't work here.

We deserialize string into a dynamic object because we only need a small number of those properties. We need to keep returning a dynamic from the method because it's called by a large number of programs. So, not interested in using anything but dynamic objects. The string is deserialized with:

dynamic o = JsonSerializer.Deserialize<dynamic>(jsonString);

Normally, we'd reference a value like o["media"]["track"][1]["vidHeight"]. In this case it error's out with "has some invalid arguments". Looking at "o" in the debugger, it shows a ValueKind Object and I'm not able to reference any of the properties directly.

ValueKind = Object : "{"media": {"@ref": "d:.......... etc.

Newtonsoft deserializes this string fine using:

o = JsonConvert.DeserializeObject<dynamic>(jsonString);

But we'd like to go the System.Text.Json route and not have the dependency on Newtonsoft. It also requires us to add .ToString() to the references like:

o["media"]["track"][1]["vidHeight"].ToString();

While that doesn't look like a big deal, the dynamic object's properties are referenced in hundreds of locations in many different Windows Services, API's, Web Services, HTTP Handlers, programs, etc. that we'd like to avoid if possible.

So, how can I deserialize these strings with System.Text.Json into a dynamic object and reference them the same way, o["nameofwhatever"], as we did when using JavaScriptSerializer?

CodePudding user response:

I would argue that you should not use dynamic in both cases. For Newtonsoft JObject, JArray and JToken can be used. For System.Text.Json you should consider JsonNode API added with .NET 6:

string jsonString = @"{
      ""Date"": ""2019-08-01T00:00:00"",
      ""Temperature"": 25,
      ""Summary"": ""Hot"",
      ""DatesAvailable"": [
        ""2019-08-01T00:00:00"",
        ""2019-08-02T00:00:00""
      ],
      ""TemperatureRanges"": {
          ""Cold"": {
              ""High"": 20,
              ""Low"": -10
          },
          ""Hot"": {
              ""High"": 60,
              ""Low"": 20
          }
      }
    }
    ";

// Create a JsonNode DOM from a JSON string.
JsonNode forecastNode = JsonNode.Parse(jsonString)!;
var value = forecastNode["TemperatureRanges"]["Hot"]["High"].ToString();
Assert.AreEqual("60", value);
var date = forecastNode["DatesAvailable"][0].GetValue<string>(); // can use when you know it is a string
Assert.AreEqual("2019-08-01T00:00:00" ,date);

CodePudding user response:

From the documentation : Using type dynamic

The dynamic type is a static type, but an object of type dynamic bypasses static type checking. In most cases, it functions like it has type object. At compile time, an element that is typed as dynamic is assumed to support any operation.

A variable dynamic allow to bypass the compiler type checking, but behind the hood the variable reference a real object instance.

With Newtonsoft.Jons :

dynamic dyn = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(jsonString)!;
Console.WriteLine(dyn.GetType());
// Newtonsoft.Json.Linq.JObject

var result = dyn["media"]["track"][1]["vidHeight"];
Console.WriteLine(result.GetType());
// Newtonsoft.Json.Linq.JValue

string str = (string)result;

Behind the hood, the dynamic variable reference a Newtonsoft.Json.Linq.JObject instance. When you use the dynamic variable, you use a Newtonsoft.Json.Linq.JObject.

Now, you want switch to System.Text.Json. But it has different types with different methods :

dynamic dyn = System.Text.Json.JsonSerializer.Deserialize<dynamic>(jsonString)!;
Console.WriteLine(dyn.GetType());
// System.Text.Json.JsonElement

var ab = dyn["media"]["track"][1]["vidHeight"];
// Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:
// 'The best overloaded method match for 'System.Text.Json.JsonElement.this[int]' has some invalid arguments'

This code throw a exception, because the type System.Text.Json.JsonElement haven't the indexer this[int].

A solution is to return a custom type that implement all expected members used by your clients.

A minimal example :

public class Result
{
    private readonly JsonElement _element;

    public Result(JsonElement element)
    {
        _element = element;
    }

    public dynamic this[string index]
    {
        get
        {
            if (_element.ValueKind != JsonValueKind.Object)
                throw new InvalidOperationException("Not a json object");
            var result = _element.GetProperty(index);
            return Parse(result);
        }
    }

    public dynamic this[int index]
    {
        get
        {
            if (_element.ValueKind != JsonValueKind.Array)
                throw new InvalidOperationException("Not a json array");
            var result = _element[index];
            return Parse(result);
        }
    }

    private dynamic Parse(JsonElement element)
    {
        if (element.ValueKind == JsonValueKind.String)
            return element.GetString()!;
        return new Result(element);
    }
}

var document = System.Text.Json.JsonDocument.Parse(jsonString);
dynamic dyn = new Result(document.RootElement);
Console.WriteLine(dyn.GetType());
// ConsoleApp.Result
var result = dyn["media"]["track"][1]["vidHeight"];
Console.WriteLine(result.GetType());
// System.String

I join @Guru, I also advocate to avoid use it. In your case, I will keep the method that return dynamic for retrocompatibility reason, but with the tag [Obsolete]. And I will push an alternative.

  • Related