Search code examples
linkerjson.netvisual-studio-2022.net-7.0self-contained

JsonConvert.DeserializeObjects does not work with trim unused code publish setting


The method JsonConvert.DeserializeObjects works when "Trim unused code" publish setting is off. When I turn this setting on I get:

Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type MyNamespace.MyObject. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'namespace', line 1, position 13.

It is a simple object, with no constructors. I have tried adding a blank constructor without arguments, nothing has changed.

Publish settings

  • Configuration: Release | Any CPU
  • Target Framework: net7.0-windows10.0.22621.0 -- using net7.0 without OS specific targeting also causes this problem
  • Deployment mode: Self - contained
  • Target runtime: win - x64
  • Produce single file: ON
  • Enable ReadyToRun compilation: ON
  • Trim unused code: ON - turning off this setting doubles the file size but the issue goes away

This is a similar question to JsonConvert.DeserializeObjects does not work after Linking SDK and User Assemblies but for Windows. The solution in that question does not work for me.

Does anyone have any workarounds for this problem?

Updated:

Bellow is a minimal example:

using Newtonsoft.Json;

namespace ConsoleApp1;

internal class Program
{
    static void Main( string[] args )
    {
        var res = JsonConvert.DeserializeObject<Response>( "{ \"blah\": [ { \"name\": \"test\", \"id\": \"test\" }, { \"name\": \"test2\", \"id\": \"test2\" } ] }" );
    }
}

public class Blah
{
    [JsonProperty( "name", NullValueHandling = NullValueHandling.Ignore )]
    public string Name { get; set; } = "";

    [JsonProperty( "id", NullValueHandling = NullValueHandling.Ignore )]
    public string Id { get; set; } = "";
}

public class Response
{
    [JsonProperty( "blah", NullValueHandling = NullValueHandling.Ignore )]
    public List<Blah> SomeList { get; } = new List<Blah>();
}

Above code runs fine when compiled normally (either Debug or Release). When published with above mentioned settings it throws an error:

Unhandled exception. Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type ConsoleApp1.Response. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'blah', line 1, position 9. at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject(JsonReader, JsonObjectContract, JsonProperty , JsonProperty , String , Boolean& ) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader , Type, JsonContract, JsonProperty, JsonContainerContract, JsonProperty, Object) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader , Type, JsonContract, JsonProperty, JsonContainerContract, JsonProperty, Object) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader , Type, Boolean) at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader , Type) at Newtonsoft.Json.JsonConvert.DeserializeObject(String , Type, JsonSerializerSettings) at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String , JsonSerializerSettings) at ConsoleApp1.Program.Main(String[]) in C:\Users\username\source\repos\MREJsonIssue\ConsoleApp1\Program.cs:line 9

Update 2:

Targeting .NET 6.0 does not produce this issue.


Solution

  • I have found two potential workarounds:

    1. Mark the main method with explicit DynamicDependency attribute:

      [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Response))]
      [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Blah))]
      static void Main( string[] args )
      
    2. Moving classes to a separate assembly and marking it with <IsTrimmable>true</IsTrimmable> and <TrimMode>copyused</TrimMode>

    3. Specifying <TrimMode>partial</TrimMode> and excluding assembly from trimming by or rooting it (not sure what the difference here):

      <ItemGroup>
         <TrimmerRootAssembly Include="ConsoleApp1" />
         <!-- or -->
         <!--<TrimmableAssembly Remove="ConsoleApp1" />-->
      </ItemGroup>
      

    P.S. there was another workaround (😁) - using reflection to get both types constructors explicitly in the program (Console.WriteLine(typeof(Response).GetConstructors().Length); and Console.WriteLine(typeof(Blah).GetConstructors().Length);)

    Also check out the docs.