I'm preparing a migration from ASP.NET Core 2.2 to 3.0.
As I don't use any 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 of how it could look like (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")
The workaround can also be as simple as this (supports multi-level JSON as well):
Output:
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:
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 goThat's because for a type
and a JSON
we will get
updateObject.Value == 0
. Now we don't know if0
is the new updated value or if it just wasn't updated, so we need to know exactly which propertiesjsonSource
contains.Fortunately, the
System.Text.Json
API allows us to examine the structure of the parsed JSON.We can now enumerate over all properties and copy them.
We will copy the value using reflection:
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 acceptingType
as a parameter inPopulateObject
).Joining it all together we get:
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 theJson.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:
To make this more robust you'd either have to have a separate
PopulateObjectDeep
method or passPopulateObjectOptions
or something similar with a deep/shallow flag.EDIT 2:
The point of deep-copying is so that if we have an object
and populate it with
we'd get
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 nowDeserialize
and you have to dig deeper to get to aJsonElement
's value. There is an active issue in the corefx repo to allow direct deserialization of aJsonElement
. Right now the closest solution is to useGetRawText()
. I went ahead and edited the code above to work, leaving the old version struck-through.Here is some sample code that does it. It's using the new Utf8JsonReader struct so it populates the object at the same time it parses it. It supports JSON/CLR types equivalence, nested objects (creates if they don't exist), lists and arrays.
Note it doesn't support all of what you would probably expect, but you can override or customize it. Things that could be added: 1) naming convention. You'd have to override the GetProperty method. 2) dictionaries or expando objects. 3) performance can be improved because it uses Reflection instead of MemberAccessor/delegate techniques
If you already use AutoMapper in your project or don't mind having dependency on it, you can merge objects in a following way:
If its just one usage and you don't want to add extra dependencies / lots of code, you don't mind a bit of inefficiency and I've not missed something obvious, you can just use:
I am not sure if this will fix your problem, but it should work as a temporary workaround. All I did was write a simple class with a populateobject method in it.
I put it into a console app for testing purposes. Below is the entire app if you would like to test it yourself.
Json File Contents:
st1.json
st2.json