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

System.Text.Json deserialize fails on List<Tuple<,>> with trimming #73169

Closed
michaldobrodenka opened this issue Aug 1, 2022 · 9 comments
Closed

Comments

@michaldobrodenka
Copy link

Description

Class:

public class Example
{
public List<Tuple<double, string>> Values { get; set; }
}

fails to deserialize when published as self contained and trim mode link. (linux-arm, I haven't tried other platforms). When using trim mode CopyUsed - everything works, but my app is almost 50% bigger.

json example:

{"Values":[{"Item1":10,"Item2":"10"}]}

Reproduction Steps

small example compiled as self contained and

JsonSerializer.Deserialize<Example>(str);

Expected behavior

To deserialize json.

Actual behavior

Exception:

JSON Deserialize exception: System.ArgumentNullException: Value cannot be null. (Parameter 'obj')
   at System.OrdinalIgnoreCaseComparer.GetHashCode(String )
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ParameterLookupKey.GetHashCode()
   at System.Collections.Generic.Dictionary`2.FindValue(TKey )
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.InitializeConstructorParameters(JsonParameterInfoValues[] , Boolean )
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo..ctor(Type , JsonConverter , Type , JsonSerializerOptions )
   at System.Text.Json.JsonSerializerOptions.<InitializeForReflectionSerializer>g__CreateJsonTypeInfo|112_0(Type , JsonSerializerOptions )
   at System.Text.Json.JsonSerializerOptions.GetClassFromContextOrCreate(Type )
   at System.Text.Json.JsonSerializerOptions.GetOrAddClass(Type )
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.get_ElementTypeInfo()
   at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , TCollection& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object , ReadStack& , Utf8JsonReader& )
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , TCollection& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object , ReadStack& , Utf8JsonReader& )
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object , ReadStack& , Utf8JsonReader& )
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , TCollection& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& , JsonSerializerOptions , ReadStack& )
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 , JsonTypeInfo , Nullable`1 )
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 , JsonTypeInfo )
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String , JsonSerializerOptions )

Regression?

Yes. It was working in NET5. After installing NET6 it doesn't work in NET5. Net6 linker is used to compile NET5 project?

Known Workarounds

Use TrimMode=CopyUsed

Configuration

-r linux-arm -p:PublishTrimmed=True -p:TrimMode=CopyUsed --self-contained true -c Release -f net6.0

Other information

No response

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Aug 1, 2022
@ghost
Copy link

ghost commented Aug 1, 2022

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Class:

public class Example
{
public List<Tuple<double, string>> Values { get; set; }
}

fails to deserialize when published as self contained and trim mode link. (linux-arm, I haven't tried other platforms). When using trim mode CopyUsed - everything works, but my app is almost 50% bigger.

json example:

{"Values":[{"Item1":10,"Item2":"10"}]}

Reproduction Steps

small example compiled as self contained and

JsonSerializer.Deserialize<Example>(str);

Expected behavior

To deserialize json.

Actual behavior

Exception:

JSON Deserialize exception: System.ArgumentNullException: Value cannot be null. (Parameter 'obj')
   at System.OrdinalIgnoreCaseComparer.GetHashCode(String )
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.ParameterLookupKey.GetHashCode()
   at System.Collections.Generic.Dictionary`2.FindValue(TKey )
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.InitializeConstructorParameters(JsonParameterInfoValues[] , Boolean )
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo..ctor(Type , JsonConverter , Type , JsonSerializerOptions )
   at System.Text.Json.JsonSerializerOptions.<InitializeForReflectionSerializer>g__CreateJsonTypeInfo|112_0(Type , JsonSerializerOptions )
   at System.Text.Json.JsonSerializerOptions.GetClassFromContextOrCreate(Type )
   at System.Text.Json.JsonSerializerOptions.GetOrAddClass(Type )
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.get_ElementTypeInfo()
   at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , TCollection& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object , ReadStack& , Utf8JsonReader& )
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , TCollection& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object , ReadStack& , Utf8JsonReader& )
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object , ReadStack& , Utf8JsonReader& )
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , TCollection& )
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& , Type , JsonSerializerOptions , ReadStack& , T& )
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& , JsonSerializerOptions , ReadStack& )
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 , JsonTypeInfo , Nullable`1 )
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 , JsonTypeInfo )
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String , JsonSerializerOptions )

Regression?

Yes. It was working in NET5. After installing NET6 it doesn't work in NET5. Net6 linker is used to compile NET5 project?

Known Workarounds

Use TrimMode=CopyUsed

Configuration

-r linux-arm -p:PublishTrimmed=True -p:TrimMode=CopyUsed --self-contained true -c Release -f net6.0

Other information

No response

Author: michaldobrodenka
Assignees: -
Labels:

area-System.Text.Json

Milestone: -

@eiriktsarpalis
Copy link
Member

Stacktrace appears related to #58690.

@agocke
Copy link
Member

agocke commented Aug 1, 2022

@michaldobrodenka did your app produce trim warnings when you were linking?

@MichalStrehovsky
Copy link
Member

Yes. It was working in NET5. After installing NET6 it doesn't work in NET5. Net6 linker is used to compile NET5 project?

Yes, the trimming is done by the SDK. It uses compatible settings, but the underlying trimming optimizations can be different. If this is indeed the same issue as analyzed in #58690, the underlying problem is that one of the new trimming optimizations is to remove parameter names of methods that were not visible targets of reflection and this is breaking System.Text.Json.

Reflection-based System.Text.Json has a lot of other trimming related problems. The only configuration of System.Text.Json that is well-supported with trimming is the source generated one (@eiriktsarpalis do we document this somewhere?). Anything else might require extra work in terms of trimming annotations (the publishing process will generate warnings and such apps need to be thoroughly re-tested after trimming). The annotation required here is likely to make it visible that the constructors of Tuple<,> are reflection-accessed.

@michaldobrodenka did your app produce trim warnings when you were linking?

If the target framework is .NET 5, we wouldn't produce warnings because at that time trimming was in preview and the warnings were suppressed.

Cc @vitek-karas @sbomer

@michaldobrodenka
Copy link
Author

michaldobrodenka commented Aug 2, 2022

ok, I found a nice workaround.

instead of List<Tuple<double, string>> I use List<ExampleValueItem> and ValueItem is

    public class ExampleValueItem
    {
        public double Item1 { get; set; }

        public string Item2 { get; set; }
    }

Since I don't need generics here it's ok for me. For backward compatibility members are named Item1/2, but that's ok. In future I'll try not to be lazy and create class for use case like this.

My app is back on linking.

Interesting is, that excluding System.Text.Json from trimming was not enough.

@MichalStrehovsky
Copy link
Member

What was interesting that excluding System.Text.Json from trimming was not enough.

The problem is in removing parameter names on the constructor of the Tuple class and that one is in CoreLib. So you would need to disable trimming on CoreLib.

My app is back on linking.

The workaround works because ExampleValueItem is now defined in your assembly and I assume your assembly is not manifested as trimmable and it's not trimmed as a result. (By default, pretty much only the framework is trimmed.)

It would be the best to use the source generated Json. It will avoid problems like this (exception that is not possible to reason about or root cause easily).

@eiriktsarpalis
Copy link
Member

The only configuration of System.Text.Json that is well-supported with trimming is the source generated one (eiriktsarpalis do we document this somewhere?).

Yes, I would say it is fairly well-documented both in the API and conceptual doc level. See also https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation-modes

ok, I found a nice workaround.

I would still recommend using the source generator in .NET 6 since the reflection-based serialization modes are not supported with trimming and could fail in unexpected ways.

I think we can close this as by-design. We should still update the reflection-based serializer to better anticipate trimmed parameter names and fail with a helpful error message, which is tracked by #58690.

@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Aug 2, 2022
@michaldobrodenka
Copy link
Author

@MichalStrehovsky thank you for your comprehensive answer! really appreciate it.

I'll stay with my custom non generic class and will avoid serializing Tuples.

@agocke
Copy link
Member

agocke commented Aug 2, 2022

One more note that reflection-based serializers are known to be incompatible with trimming and the recommendation is to use a source generated replacement, like the source-generated version of System.Text.Json that Eirik linked to.

@ghost ghost locked as resolved and limited conversation to collaborators Sep 1, 2022
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

4 participants