Can I deserialize Json with private constructor using System.Text.Json?
Asked Answered
B

3

20

Wondering if possible to have private constructors and use the new System.Text.Json serializer.

public class MyModel
{
    public string Name { get; set; }
    public string Data { get; set; }

    private MyModel() 
    {
        // use me for when deserializing
    }

    public MyModel(string name, string data)
    {
        Name = name;
        Data = data;
    }
}

A simple round trip.

var model = new MyModel("doo", "doo");
var json = JsonSerializer.Serialize(model, new JsonSerializerOptions
{
    WriteIndented = true
});

// no to go because of there is no parameterless constructor defined for this object.
var rehydrated = JsonSerializer.Deserialize<MyModel>(json);
Bufordbug answered 28/9, 2019 at 15:2 Comment(1)
There is breaking change between .net core 3.1 and .net core 5.0. Please see the following - learn.microsoft.com/en-us/dotnet/core/compatibility/…. It also suggests implementing a JsonConverter<T> if a public constructor cannot be added.Vladivostok
W
5

It would appear the answer is "No," or at least, "Not Yet".

This is a known limitation of the System.Text.Json serializer for [System.Text.Json] v1. We plan to support this in the future. -ashonkhan

You can write a custom converter for this...For the [ASP.NET Core] 3.0 release, there is no planned additional support for calling a non-default constructor during deserialization. That would have to be done by a custom converter. -steveharter

The custom converter option linked would allow you to use whatever API you do have to build the object, but isn't the same as what, say, Newtonsoft.Json or Entity Framework can do by fiddling with reflection and private constructors, so probably not what you were looking for.

Wennerholn answered 25/11, 2019 at 19:26 Comment(2)
Is there any examples of implementing a custom converter for a class where all the constructors are private?Vladivostok
@GaryChan Before .NET 7.0 you can't but from .NET 7.0 you can. Please check my answer.Alienee
A
22

Update in .NET 8.0

Just add JsonConstructorAttribute to the private constructor as follows:

public class Employee
{
   [JsonConstructor] // This will work from .NET 8.0
   private Employee()
   {
   }

   private Employee(int id, string name)
   {
     Id = id;
     Name = name;
   }

   [JsonInclude]
   public int Id { get; private set; }

   [JsonInclude]
   public string Name { get; private set; }

   public static Employee Create(int id, string name)
   {
     Employee employee = new Employee(id, name);
     return employee;
   }
}

Update in .NET 7.0

From .NET 7.0, deserialization can be done with a private parameterless constructor by writing your own ContractResolver as follows:

public class PrivateConstructorContractResolver : DefaultJsonTypeInfoResolver
{
   public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
   {
       JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);

       if (jsonTypeInfo.Kind == JsonTypeInfoKind.Object && jsonTypeInfo.CreateObject is null)
       {
         if (jsonTypeInfo.Type.GetConstructors(BindingFlags.Public | BindingFlags.Instance).Length == 0)
         {
            // The type doesn't have public constructors
            jsonTypeInfo.CreateObject = () => 
                Activator.CreateInstance(jsonTypeInfo.Type, true);
         }
       }

      return jsonTypeInfo;
   }
}

Use as follows:

private static void Main(string[] args)
{
    JsonSerializerOptions options = new JsonSerializerOptions
    {
       TypeInfoResolver = new PrivateConstructorContractResolver()
    };

    Employee employee = Employee.Create(1, "Tanvir");
    string jsonString = JsonSerializer.Serialize(employee);
    Employee employee1 = JsonSerializer.Deserialize<Employee>(jsonString , options);
}

public class Employee
{
   private Employee()
   {
   }

   private Employee(int id, string name)
   {
     Id = id;
     Name = name;
   }

   [JsonInclude]
   public int Id { get; private set; }

   [JsonInclude]
   public string Name { get; private set; }

   public static Employee Create(int id, string name)
   {
     Employee employee = new Employee(id, name);
     return employee;
   }
}
Alienee answered 2/9, 2022 at 14:16 Comment(3)
Thank you, I had missed this new feature in the RC.Busyness
I went with this clean solution, but I had to modify the ContractResolver so that it wasn't required to have no public constructors. I actually created a static HashSet that I could register Type.FullName with so the ContractResolver could selectively designate which classes I'd force JsonSerializer to use the private constructor.Foxing
You can also do this by adding a type resolver modifier, which lets you have many JsonTypeInfo resolvers instead of just 1. It only consists of your first if and everything under itBespoke
W
5

It would appear the answer is "No," or at least, "Not Yet".

This is a known limitation of the System.Text.Json serializer for [System.Text.Json] v1. We plan to support this in the future. -ashonkhan

You can write a custom converter for this...For the [ASP.NET Core] 3.0 release, there is no planned additional support for calling a non-default constructor during deserialization. That would have to be done by a custom converter. -steveharter

The custom converter option linked would allow you to use whatever API you do have to build the object, but isn't the same as what, say, Newtonsoft.Json or Entity Framework can do by fiddling with reflection and private constructors, so probably not what you were looking for.

Wennerholn answered 25/11, 2019 at 19:26 Comment(2)
Is there any examples of implementing a custom converter for a class where all the constructors are private?Vladivostok
@GaryChan Before .NET 7.0 you can't but from .NET 7.0 you can. Please check my answer.Alienee
D
0

you need to use a JsonConstructor attribute

check the link below

https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-5-0

Decanal answered 13/7, 2021 at 14:58 Comment(2)
JsonConstructor attribute don't work with private constructors, only works when you have multiple public constructors and wants to specify one of them for deserializations.Dumyat
It does from .net 8Luxuriant

© 2022 - 2024 — McMap. All rights reserved.