Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IJSRuntime ignores custom json serializer in server side blazor project

In my server side blazor project (core 3, preview6) I'm trying to invoke javascript with an instance of my class. You can do this by injecting a IJSRuntime and calling InvokeAsync on it (see here).

Because I need to get rid of all the null properties (the js library that should handle my object (chartJs) cannot handle null values) and because I have some custom serialization (for enums that can have different datatypes), I hacked it to work with an ExpandoObject (that was not my idea but it works).

I first serialized my object with json.net so I could define NullValueHandling = NullValueHandling.Ignore and so all the custom converters got used.

Then I used json.net again to parse it to an ExpandoObject. I of course did that because otherwise the values that aren't in the json would be null in the c# instance but this way they aren't present at all.

This ExpandoObject which doesn't have the null values and which has the correct enum values, is then used to invoke the javascript. This way there are no null values when it gets to the javascript engine.

Since in preview6 json.net isn't used internally anymore and the new json serializer (from System.Text.Json) cannot handle ExpandoObjects, I got a runtime error when I updated from preview5 to preview6, saying that ExpandoObject isn't supported. No big deal, I made a function to convert an ExpandoObject to a Dictionary<string, object> which can be serialized without a problem.

Read this question to see more about how I did this and to see some examples. This might be a good idea if you don't quite understand what I did. Also it contains the json that I will add in here as well along with the c# model and other explanations.

This works but I thought that maybe if I would be able to get back to json.net I could completely remove the ExpandoObject because firstly it would use the custom converters I wrote for my special enums and secondly I could specify NullValueHandling = NullValueHandling.Ignore globally (ps. would this have sideeffects?).

I followed the ms docs on how to use json.net again and added this to the ConfigureServies method (services.AddRazorPages() was already there):

services.AddRazorPages()
    .AddNewtonsoftJson(o =>
    {
        o.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
        o.SerializerSettings.ContractResolver = new DefaultContractResolver
        {
            NamingStrategy = new CamelCaseNamingStrategy(true, false)
        };
    });

Sadly it did not work and directly invoking js with the object led to the exact same json (see below) as before I added this.

Then I tried using an ExpandoObject directly because I knew the new json serializer from .net can't handle that. As expected it threw an exception and the stacktrace shows no signs of json.net. This confirms that it's not actually using json.net like I told it to.

at System.Text.Json.Serialization.JsonClassInfo.GetElementType(Type propertyType, Type parentType, MemberInfo memberInfo) at System.Text.Json.Serialization.JsonClassInfo.CreateProperty(Type declaredPropertyType, Type runtimePropertyType, PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonClassInfo.AddProperty(Type propertyType, PropertyInfo propertyInfo, Type classType, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonClassInfo..ctor(Type type, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializerOptions.GetOrAddClass(Type classType) at System.Text.Json.Serialization.JsonSerializer.GetRuntimeClassInfo(Object value, JsonClassInfo& jsonClassInfo, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializer.HandleEnumerable(JsonClassInfo elementClassInfo, JsonSerializerOptions options, Utf8JsonWriter writer, WriteStack& state) at System.Text.Json.Serialization.JsonSerializer.Write(Utf8JsonWriter writer, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonSerializer.WriteCore(PooledByteBufferWriter output, Object value, Type type, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializer.WriteCoreString(Object value, Type type, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializer.ToString[TValue](TValue value, JsonSerializerOptions options) at Microsoft.JSInterop.JSRuntimeBase.InvokeAsync[T](String identifier, Object[] args) at ChartJs.Blazor.ChartJS.ChartJsInterop.GetJsonRep(IJSRuntime jSRuntime, Object obj)

I then also tried just changing the options from the normal serializer to see if that would change anything. I used this instead of AddNewtonsoftJson:

services.AddRazorPages()
    .AddJsonOptions(o => o.JsonSerializerOptions.IgnoreNullValues = true);

Interestingly this still gave me the same result.

I will add the json that got produced along with what should be produced. You can also find this in the CodeReview question I mentioned earlier.

The following json is what I get:

It stays the same when

  • I don't specify anything in the ConfigureServices method.
  • I specify to use Newtonsoft.Json through AddNewtonsoftJson.
  • I specify the JsonOptions from .net through AddJsonOptions.
{
    "options": {
        "someInt": 2,
        "someString": null,
        "axes": [
            {
                "someString": null
            },
            {
                "someString": "axisString"
            }
        ]
    },
    "data": {
        "data": [
            1,
            2,
            3,
            4,
            5
        ],
        "someString": "asdf",
        "someStringEnum": {}   <-- this is one of those special enums with a custom converter
    }
}

This is what I should get. Notice all the null values are removed and the custom converter is used. I was only able to get this with my hacky solution that uses the ExpandoObject.

{
    "options": {
        "someInt": 2,
        "axes": [
            {},
            {
                "someString": "axisString"
            }
        ]
    },
    "data": {
        "data": [
            1,
            2,
            3,
            4,
            5
        ],
        "someString": "asdf",
        "someStringEnum": "someTestThing"   <-- this is one of those special enums with a custom converter
    }
}

So why is it still using System.Text.Json instead of Newtonsoft.Json even though I instructed it to? What else do I have to add to make it use json.net?

like image 658
Joelius Avatar asked Nov 06 '22 15:11

Joelius


1 Answers

As correctly mentioned in the comments by @dbc, IJSRuntime always uses System.Text.Json. I have confirmed this by asking the asp.net team about it (see my github issue).

I submitted a github feature request which might make this possible in future .NET versions.

like image 113
Joelius Avatar answered Nov 09 '22 22:11

Joelius