JamesNK / Newtonsoft.Json

Json.NET is a popular high-performance JSON framework for .NET
https://www.newtonsoft.com/json
MIT License
10.76k stars 3.25k forks source link

Generic Attributes -> Generic types are not valid. #2929

Closed XtremeOwnageDotCom closed 8 months ago

XtremeOwnageDotCom commented 8 months ago

So, I was looking to find a way to better document expected responses to various messages. So, I created an attribute to document what the expected response would be for a particular message.

Source/destination types

/// <summary>
/// Defines what type of message should be expected in return.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ExpectedResponseTypeAttribute<T> : Attribute
{
}

It would typically be used, like so.

[ExpectedResponse<MyResponse>]
public class MyRequest {}

public class MyResponse {}

However, it appears Newtonsoft does not appreciate this attribute, containing a generic type.

Source/destination JSON

The resulting JSON does not contain anything regarding the issue here. Attributes are not included in generated JSON at all.

Expected behavior

Expected- It just works.

Especially since the attribute itself, is not actually included in the generated json, nor would you expect it to be deserialized.

When you serialize a class, its attributes are not included in the resulting JSON, as such, I don't expect exceptions to occur when deserializing something, that includes an attribute that would not be used anyways.

This feature was introduced in c# 11

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/generic-attributes

Actual behavior

at System.RuntimeTypeHandle.CreateCaInstance(RuntimeType type, IRuntimeMethodInfo ctor)
at System.Reflection.CustomAttribute.GetCustomAttributes(RuntimeModule decoratedModule, Int32 decoratedMetadataToken, Int32 pcaCount, RuntimeType attributeFilterType, Boolean mustBeInheritable, IList derivedAttributes, Boolean isDecoratedTargetSecurityTransparent)
at System.Reflection.CustomAttribute.GetCustomAttributes(RuntimeType type, RuntimeType caType, Boolean inherit)
at Newtonsoft.Json.Utilities.ReflectionUtils.GetAttributes(Object attributeProvider, Type attributeType, Boolean inherit)
at Newtonsoft.Json.Serialization.JsonTypeReflector.GetAssociateMetadataTypeFromAttribute(Type type)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
at Newtonsoft.Json.Serialization.JsonTypeReflector.GetAttribute[T](Type type)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
at Newtonsoft.Json.Serialization.DefaultContractResolver.CreateContract(Type objectType)
...

Steps to reproduce

  public class TestAttribute<T> : Attribute { }
  [Test<string>]
  public class TestClass{}

      static void main()
      {
          var output = Newtonsoft.Json.JsonConvert.SerializeObject(new TestClass()); <- Exception HERE.
          Console.WriteLine(output);
          JsonConvert.DeserializeObject<TestClass>(output);
      }

Resulting exception-

   at System.RuntimeTypeHandle.CreateCaInstance(RuntimeType type, IRuntimeMethodInfo ctor)
   at System.Reflection.CustomAttribute.GetCustomAttributes(RuntimeModule decoratedModule, Int32 decoratedMetadataToken, Int32 pcaCount, RuntimeType attributeFilterType, Boolean mustBeInheritable, IList derivedAttributes, Boolean isDecoratedTargetSecurityTransparent)
   at System.Reflection.CustomAttribute.GetCustomAttributes(RuntimeType type, RuntimeType caType, Boolean inherit)
   at Newtonsoft.Json.Utilities.ReflectionUtils.GetAttributes(Object attributeProvider, Type attributeType, Boolean inherit)
   at Newtonsoft.Json.Serialization.JsonTypeReflector.GetAssociateMetadataTypeFromAttribute(Type type)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Newtonsoft.Json.Serialization.JsonTypeReflector.GetAttribute[T](Type type)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Newtonsoft.Json.Serialization.DefaultContractResolver.CreateContract(Type objectType)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Newtonsoft.Json.Serialization.DefaultContractResolver.ResolveContract(Type type)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
   at Newtonsoft.Json.JsonConvert.SerializeObjectInternal(Object value, Type type, JsonSerializer jsonSerializer)
   at Newtonsoft.Json.JsonConvert.SerializeObject(Object value)

With Error Message: "Generic types are not valid."

elgonzo commented 8 months ago

Not reproducible using Newtonsoft.Json 13.0.3 and the sample code given in your report. The sample code outputs the expected {} and throws no exception.

For reference, these are the project settings i used:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>
</Project>
XtremeOwnageDotCom commented 8 months ago

Not reproducible using Newtonsoft.Json 13.0.3 and the sample code given in your report. The sample code outputs the expected {} and throws no exception.

    <TargetFramework>net8.0</TargetFramework>

I wonder, if perhaps, this is something specific to older framework apps. I noticed this issue on an app running .net framework 4.8.

The message it received, was successfully serialized without issue from an app running .NET 8.

But, upon deserializing it, it failed on the framework 4.8 app... Let me test this...

elgonzo commented 8 months ago

Yeah, .NET Framework 4.8 is quite likely way too old. I believe (although i am not 100% certain) generic attributes require support by the runtime, particularly how attribute data is represented in the CLR.

XtremeOwnageDotCom commented 8 months ago

Testing with my example code-

using ConsoleApp1;
using Newtonsoft.Json;

var output = Newtonsoft.Json.JsonConvert.SerializeObject(new TestClass());
Console.WriteLine(output);
JsonConvert.DeserializeObject<TestClass>(output);
Console.WriteLine("Worked here.");

Yields this, on .NET 8

{}
Worked here.

Making a few minor changes.... such as enabling preview language version, and setting framework to 4.8....

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net48</TargetFramework>
        <LangVersion>preview</LangVersion>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
    </ItemGroup>

</Project>

image

So, confirmed, this is specific to .net framework, and does not affect newer .NET.

elgonzo commented 8 months ago

It's not an issue of the Newtonsoft.Json library and not something the library can resolve. Generic attribute types are basically a feature that is dependent on the runtimes of recent .NET versions and/or their respective BCL implementations.

Note that <LangVersion>preview</LangVersion> enables preview features the used C# compiler supports, and that includes enabling language features the compiler supports but the targeted framework version does not support.

XtremeOwnageDotCom commented 8 months ago

Issue closed.

Resolution, Update old applications, off of .net framework.