Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonSerializer.Serialize throws when encountering nullable type in netstandard code paths #39208

Closed
chucker opened this issue Jul 12, 2020 · 4 comments
Assignees
Milestone

Comments

@chucker
Copy link

chucker commented Jul 12, 2020

Minimal repro:

  1. Create a .NET Framework app
  2. Reference S.T.Json 3.2
  3. Run this code:
    class Program
    {
        static void Main(string[] args)
        {
            var json = JsonSerializer.Serialize(new Foo
            {
                Anchor = new MyStruct { X = 1 }
            });

            Console.WriteLine(json);
        }
    }

    struct MyStruct
    {
        public int X { get; set; }
    }

    class Foo
    {
        public MyStruct? Anchor { get; set; }
    }

Exception:

Unhandled Exception: System.ArgumentException: GenericArguments[0], 'System.Nullable`1[ConsoleApp12.MyStruct]', on 'System.Func`2[System.Object,TProperty] CreateStructPropertyGetter[TClass,TProperty](GetPropertyByRef`2)' violates the constraint of type 'TClass'. ---> System.Security.VerificationException: Method System.Text.Json.ReflectionMemberAccessor.CreateStructPropertyGetter: type argument 'System.Nullable`1[ConsoleApp12.MyStruct]' violates the constraint of type parameter 'TClass'.
   at System.RuntimeMethodHandle.GetStubIfNeeded(RuntimeMethodHandleInternal method, RuntimeType declaringType, RuntimeType[] methodInstantiation)
   at System.Reflection.RuntimeMethodInfo.MakeGenericMethod(Type[] methodInstantiation)
   --- End of inner exception stack trace ---
   at System.RuntimeType.ValidateGenericArguments(MemberInfo definition, RuntimeType[] genericArguments, Exception e)
   at System.Reflection.RuntimeMethodInfo.MakeGenericMethod(Type[] methodInstantiation)
   at System.Text.Json.ReflectionMemberAccessor.CreatePropertyGetter[TClass,TProperty](PropertyInfo propertyInfo)
   at System.Text.Json.JsonPropertyInfoCommon`4.Initialize(Type parentClassType, Type declaredPropertyType, Type runtimePropertyType, Type implementedPropertyType, PropertyInfo propertyInfo, Type elementType, JsonConverter converter, JsonSerializerOptions options)

Note that this does not reproduce in netcoreapp3.1 \ net5.0.


Describe the bug

When an object that is passed to JSRuntime.InvokeVoidAsync's args contains a property that is a nullable value type (e.g., System.Drawing.Point?), and that property is set to a value, Blazor WASM throws an exception.

To Reproduce

GitHub repo: https://github.com/chucker/blazorjsnullablevaluetype

Add a simple JS logging method to index.html's head:

    <script>
        window.jsTestMethod = (data) => {
            console.log(data);
        };
    </script>

In Index.razor, add a struct, a class that takes the struct as property, and some code that instantiates the class and passes it to the JS method:

@code {
    struct MyStruct
    {
        public int X { get; set; }
    }

    class Foo
    {
        // change this to `MyStruct?` to reproduce
        public MyStruct Anchor { get; set; }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        var f = new Foo
        {
            Anchor = new MyStruct { X = 1 }
        };

        await JSRuntime.InvokeVoidAsync("jsTestMethod", f);
    }
}

This will work. Your browser will log:

[Log] {anchor: {x: 1}} (localhost, line 14)

Now, change the Anchor property to be nullable:

public MyStruct? Anchor { get; set; }

This will throw an exception (see below) before the JS method ever gets called.

Exception

crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Invalid generic arguments
      Parameter name: typeArguments
System.ArgumentException: Invalid generic arguments
Parameter name: typeArguments
  at (wrapper managed-to-native) System.Reflection.RuntimeMethodInfo.MakeGenericMethod_impl(System.Reflection.RuntimeMethodInfo,System.Type[])
  at System.Reflection.RuntimeMethodInfo.MakeGenericMethod (System.Type[] methodInstantiation) <0x2c77fd8 + 0x000d0> in <filename unknown>:0 
  at System.Text.Json.ReflectionMemberAccessor.CreatePropertyGetter[TClass,TProperty] (System.Reflection.PropertyInfo propertyInfo) <0x2c77d60 + 0x0003e> in <filename unknown>:0 
  at System.Text.Json.JsonPropertyInfoCommon`4[TClass,TDeclaredProperty,TRuntimeProperty,TConverter].Initialize (System.Type parentClassType, System.Type declaredPropertyType, System.Type runtimePropertyType, System.Type implementedPropertyType, System.Reflection.PropertyInfo propertyInfo, System.Type elementType, System.Text.Json.Serialization.JsonConverter converter, System.Text.Json.JsonSerializerOptions options) <0x2c77778 + 0x00094> in <filename unknown>:0 
  at System.Text.Json.JsonClassInfo.CreateProperty (System.Type declaredPropertyType, System.Type runtimePropertyType, System.Type implementedPropertyType, System.Reflection.PropertyInfo propertyInfo, System.Type parentClassType, System.Text.Json.Serialization.JsonConverter converter, System.Text.Json.JsonSerializerOptions options) <0x2aed3f0 + 0x002c4> in <filename unknown>:0 
  at System.Text.Json.JsonClassInfo.AddProperty (System.Type propertyType, System.Reflection.PropertyInfo propertyInfo, System.Type classType, System.Text.Json.JsonSerializerOptions options) <0x2aecf00 + 0x00072> in <filename unknown>:0 
  at System.Text.Json.JsonClassInfo..ctor (System.Type type, System.Text.Json.JsonSerializerOptions options) <0x2adb2a0 + 0x00168> in <filename unknown>:0 
  at System.Text.Json.JsonSerializerOptions.GetOrAddClass (System.Type classType) <0x2ad8d30 + 0x00036> in <filename unknown>:0 
  at System.Text.Json.JsonPropertyInfo.get_RuntimeClassInfo () <0x2c77260 + 0x0001a> in <filename unknown>:0 
  at System.Text.Json.JsonSerializer.HandleObject (System.Text.Json.JsonPropertyInfo jsonPropertyInfo, System.Text.Json.JsonSerializerOptions options, System.Text.Json.Utf8JsonWriter writer, System.Text.Json.WriteStack& state) <0x2c74cc0 + 0x001fc> in <filename unknown>:0 
  at System.Text.Json.JsonSerializer.WriteObject (System.Text.Json.JsonSerializerOptions options, System.Text.Json.Utf8JsonWriter writer, System.Text.Json.WriteStack& state) <0x2c748b8 + 0x000cc> in <filename unknown>:0 
  at System.Text.Json.JsonSerializer.Write (System.Text.Json.Utf8JsonWriter writer, System.Int32 originalWriterDepth, System.Int32 flushThreshold, System.Text.Json.JsonSerializerOptions options, System.Text.Json.WriteStack& state) <0x2afbce0 + 0x000ec> in <filename unknown>:0 
  at System.Text.Json.JsonSerializer.WriteCore (System.Text.Json.Utf8JsonWriter writer, System.Object value, System.Type type, System.Text.Json.JsonSerializerOptions options) <0x2ad8840 + 0x0008a> in <filename unknown>:0 
  at System.Text.Json.JsonSerializer.WriteCore (System.Text.Json.PooledByteBufferWriter output, System.Object value, System.Type type, System.Text.Json.JsonSerializerOptions options) <0x28d4418 + 0x00040> in <filename unknown>:0 
  at System.Text.Json.JsonSerializer.WriteCoreString (System.Object value, System.Type type, System.Text.Json.JsonSerializerOptions options) <0x28d4020 + 0x0003c> in <filename unknown>:0 
  at System.Text.Json.JsonSerializer.Serialize[TValue] (TValue value, System.Text.Json.JsonSerializerOptions options) <0x28d3bb0 + 0x0000c> in <filename unknown>:0 
  at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue] (System.String identifier, System.Threading.CancellationToken cancellationToken, System.Object[] args) <0x2c4db10 + 0x001f6> in <filename unknown>:0 
  at Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue] (System.String identifier, System.Object[] args) <0x2c4d420 + 0x0005a> in <filename unknown>:0 
  at Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync (Microsoft.JSInterop.IJSRuntime jsRuntime, System.String identifier, System.Object[] args) <0x2c4d088 + 0x00042> in <filename unknown>:0 
  at blazorjsnullablevaluetype.Pages.Index.OnAfterRenderAsync (System.Boolean firstRender) <0x2c4c528 + 0x0013e> in <filename unknown>:0 

Further technical details

  • ASP.NET Core version Microsoft.AspNetCore.App 3.1.5, Blazor 3.2.0
  • Include the output of dotnet --info
> dotnet --info .NET Core SDK (reflecting any global.json): Version: 3.1.301 Commit: 7feb845744

Runtime Environment:
OS Name: Mac OS X
OS Version: 10.15
OS Platform: Darwin
RID: osx.10.15-x64
Base Path: /usr/local/share/dotnet/sdk/3.1.301/

Host (useful for support):
Version: 3.1.5
Commit: 65cd789777

.NET Core SDKs installed:
3.1.301 [/usr/local/share/dotnet/sdk]

.NET Core runtimes installed:
Microsoft.AspNetCore.App 3.1.5 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.1.18 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.19 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.5 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

To install additional .NET Core runtimes or SDKs:
https://aka.ms/dotnet-download

(Windows is also affected.)

  • The IDE (VS / VS Code/ VS4Mac) you're running on, and it's version

VS4Mac 8.7 Preview build 1802

(VS 16.7 Preview 3.1 is also affected)

Remarks

  • Blazor Server does not appear to be affected
  • I have not tried other JS interop methods such as InvokeAsync<>.
  • I have not tried whether only properties are affected.
@ShadyNagy
Copy link

JsonSerializer.Serialize not working well with nullable or workaround needed here.

that is the problem
https://github.com/dotnet/aspnetcore/blob/c58ab9247c472aa0cb1cb9d71689d044e338c0ee/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs#L113

@ShadyNagy
Copy link

If struct MyStruct changed to class MyStruct it will work also normally.

@pranavkm pranavkm transferred this issue from dotnet/aspnetcore Jul 13, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Interop-coreclr untriaged New issue has not been triaged by the area owner labels Jul 13, 2020
@pranavkm pranavkm changed the title Blazor WASM: cannot pass nullable value type property to JS interop JsonSerializer.Serialize throws when encountering nullable type in netstandard code paths Jul 13, 2020
@layomia layomia removed the untriaged New issue has not been triaged by the area owner label Jul 16, 2020
@layomia layomia added this to the 5.0.0 milestone Jul 16, 2020
@layomia layomia self-assigned this Jul 16, 2020
@layomia layomia removed their assignment Aug 7, 2020
@steveharter
Copy link
Member

FYI I ran the code on 3.1 (netcoreapp, not netstandard) and although it doesn't throw it does have an issue serializing nullable structs.

The JSON from the test above:

        // 5.0: {"Anchor":{"X":1}}
        // 3.1: {"Anchor":{"HasValue":true,"Value":{"X":1}}}

It appears it is treating Nullable<> as an object and not the special value type it is.

@steveharter
Copy link
Member

Per @layomia this is a dup of #30843.

Strategies for workaround:

  • Move to 5.0 if possible.
  • Use a class, not a struct.
  • Write a custom converter for that struct.

@layomia layomia self-assigned this Aug 14, 2020
@layomia layomia closed this as completed Aug 14, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants