Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.Net Core 3.0 JsonSerializer populate existing object

I'm preparing a migration from ASP.NET Core 2.2 to 3.0.

As I don't use more advanced JSON features (but maybe one as described below), and 3.0 now comes with a built-in namespace/classes for JSON, System.Text.Json, I decided to see if I could drop the previous default Newtonsoft.Json.
Do note, I'm aware that System.Text.Json will not completely replace Newtonsoft.Json.

I managed to do that everywhere, e.g.

var obj = JsonSerializer.Parse<T>(jsonstring);  var jsonstring = JsonSerializer.ToString(obj); 

but in one place, where I populate an existing object.

With Newtonsoft.Json one can do

JsonConvert.PopulateObject(jsonstring, obj); 

The built-in System.Text.Json namespace has some additional classes, like JsonDocumnet, JsonElement and Utf8JsonReader, though I can't find any that take an existing object as a parameter.

Nor am I experienced enough to see how to make use of the existing one's.

There might be a possible upcoming feature in .Net Core (thanks to Mustafa Gursel for the link), but meanwhile (and what if it doesn't),...

...I now wonder, is it possible to achieve something similar as what one can do with PopulateObject?

I mean, is it possible with any of the other System.Text.Json classes to accomplish the same, and update/replace only the properties set?,... or some other clever workaround?


Here is a sample input/output of what I am looking for, and it need to be generic as the object passed into the deserialization method is of type <T>). I have 2 Json string's to be parsed into an object, where the first have some default properties set, and the second some, e.g.

Note, a property value can be of any other type than a string.

Json string 1:

{   "Title": "Startpage",   "Link": "/index", } 

Json string 2:

{   "Head": "Latest news"   "Link": "/news" } 

Using the 2 Json strings above, I want an object resulting in:

{   "Title": "Startpage",   "Head": "Latest news",   "Link": "/news" } 

As seen in above sample, if properties in the 2nd has values/is set, it replace values in the 1st (as with "Head" and "Link"), if not, existing value persist (as with "Title")

like image 839
Asons Avatar asked Jul 01 '19 11:07

Asons


People also ask

Is Newtonsoft JSON obsolete?

The Newtonsoft. Json. Schema namespace provides classes that are used to implement JSON schema. Obsolete.

Is JSON net deprecated?

Despite being deprecated by Microsoft in . NET Core 3.0, the wildly popular Newtonsoft. Json JSON serializer still rules the roost in the NuGet package manager system for . NET developers.

Should I use Newtonsoft or System text JSON?

By default, Newtonsoft. Json does case insensitive property name matching during deserialization whereas System. Text. Json does case sensitive matching (with exception in ASP.Net core where you don't need to do anything to achieve behavior like Newtonsoft.

What does JsonSerializer serialize do?

Serialize(Object, Type, JsonSerializerOptions)Converts the value of a specified type into a JSON string.

What is default JSON serializer in ASP NET Core?

With the introduction of ASP.NET Core 3.0 the default JSON serializer has been changed from Newtonsoft.Json to the native System.Text.Json. In this blog post, we will go over some basic usages of JSON serialization and deserialization.

What is the best way to deserialize JSON?

As you can see, using the new JsonSerializer is the clear winner on performance when deserializing JSON! The benchmark results show that using the new JsonSerializer in .NET Core 3 is overall more performant. I hope that this article will encourage you to look at it.

Is there a native JSON library for NET Core?

Even though there were many discussions on the JSON library for .NET Core ( link ), for most .NET users, it is good news to see a native JSON library. Also, there are several benchmark articles ( link1, link2, link3, …) which show that the System.Text.Json namespace holds a better performance in most use cases as compared to other JSON libraries.

What's new in Core 3 for JSON?

The release of .NET Core 3 today includes a brand new serializer for JavaScript Object Notation (JSON) under the System.Text.Json namespace. The release of .NET Core 3 today includes a brand new serializer for JavaScript Object Notation (JSON) under the System.Text.Json namespace. The Microsoft documentation states,


1 Answers

So assuming that Core 3 doesn't support this out of the box, let's try to work around this thing. So, what's our problem?

We want a method that overwrites some properties of an existing object with the ones from a json string. So our method will have a signature of:

void PopulateObject<T>(T target, string jsonSource) where T : class 

We don't really want any custom parsing as it's cumbersome, so we'll try the obvious approach - deserialize jsonSource and copy the result properties into our object. We cannot, however, just go

T updateObject = JsonSerializer.Parse<T>(jsonSource); CopyUpdatedProperties(target, updateObject); 

That's because for a type

class Example {     int Id { get; set; }     int Value { get; set; } } 

and a JSON

{     "Id": 42 } 

we will get updateObject.Value == 0. Now we don't know if 0 is the new updated value or if it just wasn't updated, so we need to know exactly which properties jsonSource contains.

Fortunately, the System.Text.Json API allows us to examine the structure of the parsed JSON.

using var json = JsonDocument.Parse(jsonSource).RootElement; 

We can now enumerate over all properties and copy them.

foreach (var property in json.EnumerateObject()) {     OverwriteProperty(target, property); } 

We will copy the value using reflection:

void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class {     var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);      if (propertyInfo == null)     {         return;     }      var propertyType = propertyInfo.PropertyType;     v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶     var parsedValue = JsonSerializer.Deserialize(         updatedProperty.Value.GetRawText(),          propertyType);      propertyInfo.SetValue(target, parsedValue); }  

We can see here that what we're doing is a shallow update. If the object contains another complex object as its property, that one will be copied and overwritten as a whole, not updated. If you require deep updates, this method needs to be changed to extract the current value of the property and then call the PopulateObject recursively if the property's type is a reference type (that will also require accepting Type as a parameter in PopulateObject).

Joining it all together we get:

void PopulateObject<T>(T target, string jsonSource) where T : class {     using var json = JsonDocument.Parse(jsonSource).RootElement;      foreach (var property in json.EnumerateObject())     {         OverwriteProperty(target, property);     } }  void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class {     var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);      if (propertyInfo == null)     {         return;     }      var propertyType = propertyInfo.PropertyType;     v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶     var parsedValue = JsonSerializer.Deserialize(         updatedProperty.Value.GetRawText(),          propertyType);      propertyInfo.SetValue(target, parsedValue); }  

How robust is this? Well, it certainly won't do anything sensible for a JSON array, but I'm not sure how you'd expect a PopulateObject method to work on an array to begin with. I don't know how it compares in performance to the Json.Net version, you'd have to test that by yourself. It also silently ignores properties that are not in the target type, by design. I thought it was the most sensible approach, but you might think otherwise, in that case the property null-check has to be replaced with an exception throw.

EDIT:

I went ahead and implemented a deep copy:

void PopulateObject<T>(T target, string jsonSource) where T : class =>      PopulateObject(target, jsonSource, typeof(T));  void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>     OverwriteProperty(target, updatedProperty, typeof(T));  void PopulateObject(object target, string jsonSource, Type type) {     using var json = JsonDocument.Parse(jsonSource).RootElement;      foreach (var property in json.EnumerateObject())     {         OverwriteProperty(target, property, type);     } }  void OverwriteProperty(object target, JsonProperty updatedProperty, Type type) {     var propertyInfo = type.GetProperty(updatedProperty.Name);      if (propertyInfo == null)     {         return;     }      var propertyType = propertyInfo.PropertyType;     object parsedValue;      if (propertyType.IsValueType || propertyType == typeof(string))     {         ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶         parsedValue = JsonSerializer.Deserialize(             updatedProperty.Value.GetRawText(),             propertyType);     }     else     {         parsedValue = propertyInfo.GetValue(target);         P̶o̶p̶u̶l̶a̶t̶e̶O̶b̶j̶e̶c̶t̶(̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶,̶ ̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶         PopulateObject(             parsedValue,              updatedProperty.Value.GetRawText(),              propertyType);     }      propertyInfo.SetValue(target, parsedValue); } 

To make this more robust you'd either have to have a separate PopulateObjectDeep method or pass PopulateObjectOptions or something similar with a deep/shallow flag.

EDIT 2:

The point of deep-copying is so that if we have an object

{     "Id": 42,     "Child":     {         "Id": 43,         "Value": 32     },     "Value": 128 } 

and populate it with

{     "Child":     {         "Value": 64     } } 

we'd get

{     "Id": 42,     "Child":     {         "Id": 43,         "Value": 64     },     "Value": 128 } 

In case of a shallow copy we'd get Id = 0 in the copied child.

EDIT 3:

As @ldam pointed out, this no longer works in stable .NET Core 3.0, because the API was changed. The Parse method is now Deserialize and you have to dig deeper to get to a JsonElement's value. There is an active issue in the corefx repo to allow direct deserialization of a JsonElement. Right now the closest solution is to use GetRawText(). I went ahead and edited the code above to work, leaving the old version struck-through.

like image 194
V0ldek Avatar answered Sep 23 '22 22:09

V0ldek