[C #] Some scenarios for deserializing a JSON to a type with read-only properties by "System.Text.Json"
Tags: csharp, dotnet
Published: 2021-05-01 05:39:17

Recently I feel that using the `System.Text.Json` library for serializing/deserializing with JSON is increasing in C# programming.

And using immutable object - that has read-only properties, and those properties are initialized in a constructor - is also increasing, I feel too. 
For example, like the C# class as below:

```csharp
class PersonClass
{
  public string? Name { get; }

  public int Age { get; }

  public PersonClass(string name, int age)
  {
    Name = name;
    Age = age;
  }
}
```

Of course, the `Deserialize<T>()` static method of the `JsonSerializer` class in the `System.Text.Json` library can deserialize a JSON to an immutable object such as the above C# class.

```csharp
var json = @"{""Name"":""Taro"",""Age"":23}";
var person = JsonSerializer.Deserialize<PersonClass>(json);
// person.Name -> "Taro"
// person.Age -> 23
```
The `Deserialize<T>()` static method will detect that the class has the constructor with arguments usable for deserializing. 
So by using that constructor, the `Deserialize<T>()` static method can deserialize a JSON to an immutable object like that.

To the `Deserialize<T>()` static method can detect the constructor that can use for deserializing, that constructor must have arguments that are the same-named its properties.

If it doesn't that, the exception will throw when the `Deserialize<T>()` static method invoked.

```csharp
public PersonClass(string foo, int age)
{
  Name = foo; // The argument name isn't the same as the property.
              // This will cause an unhandled exception.
  ...
```

# Deserializing will fail if the class has multiple constructors.

In some rare cases, the immutable object class has to have multiple constructors.

But if the class to be deserialized has multiple constructors, the `Deserialize<T>()` static method will **not** work expectedly.
― even if the class has the appropriate constructor overload version for deserializing.

The `Deserialize<T>()` static method will return the object instance with no errors, but the properties values still default value.

```csharp
class PersonClass
{
  ...
  public PersonClass() { } // 👈 Add this, then...
  public PersonClass(string name, int age) {...}
}

...

var json = @"{""Name"":""Taro"",""Age"":23}";
var person = JsonSerializer.Deserialize<PersonClass>(json);
// person.Name -> null ... is not "Taro"!
// person.Age -> 0 ... is not 23!
```

Because the `Deserialize<T>()` static method will try to use the default constructor if the class has it.

But any properties are read-only, so the properties of the object that deserialized are not written.

# To resolve this problem, use the `[JsonConstructor]` attribute.

We can annotate the appropriate constructor for deserialization with `[JsonConstructor]` attribute to resolve this problem.

After doing this, the `Deserialize<T>()` static method will use the `[JsonConstructor]` annotated constructor to instantiate the object. So the deserialization will work fine as the developers expected.

```csharp
class PersonClass
{
  ...
  public PersonClass() { }

  [JsonConstructor] // 👈 Adding this to the constructor for
                    //     deserialization, will resolve the problem.
  public PersonClass(string name, int age) {...}
  ...
```

# If the property has an init-only setter...

If the property has an init-only setter, the `Deserialize<T>()` static method will work fine as we expected, even if it has no `[JsonConstructor]` annotations.

```csharp
class PersonClass
{
  public string Name { get; init; }
  public int Age { get; init; }
  public PersonClass() { }
  public PersonClass(string name, int age) {...}
  ...
```

In this case, the `Deserialize<T>()` static method will use the no-arguments default constructor to instantiate the object.

And the `Deserialize<T>()` static method understands how to treat init-only setter.

So the `Deserialize<T>()` static method can write back the value from JSON to the init-setter only property.

# If the property has a private setter...

If the property has a private setter, the `Deserialize<T>()` static method will not work as fine as expected.

```csharp
class PersonClass
{
  public string Name { get; private set; }
  public int Age { get; private set; }
  public PersonClass() { }
  public PersonClass(string name, int age) {...}
  ...
```

The `Deserialize<T>()` static method will not write back the value from a JSON to that property via the private setter without explicit instruction.

In this scenario, of course, we can use the `[JsonConstructor]` approach to resolve it.

But another way, we can also apply the `[JsonInclude]` attribute to the private setter property to resolve it.

If the private setter property is annotated the `[JsonInclude]` attribute, the `Deserialize<T>()` static method will write back the value from a JSON to that property via the private setter.

```csharp
class PersonClass
{
  [JsonInclude] // 👈 Adding this attribute allows to write back
                //     the value from a JSON to this property.
  public string Name { get; private set; }

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

  public PersonClass() { }
  public PersonClass(string name, int age) {...}
  ...
```

# A little something extra

When using the "constructor initialized read-only properties" implementation pattern, considering using **the "record" type** is also one of a good option, I think.

Happy Coding! :)