Tjaz Brelih
Tjaz Brelih

Reputation: 95

JsonSerializer not using internal constructor during deserialization

I'm writing a library to access a web API used to manage data in the backend system. The library will also provide classes with logic to properly manage the data. For this reason most of the classes cannot have public setters for properties or a public constructor that accepts all the properties. Instead I'm trying to use a constructor with internal access modifier that can set all the properties.

When trying to deserialize a JSON string using System.Text.Json it will ignore the internal constructor - it will either use some other constructor or throw an exception. I've also tried annotating the internal constructor with [JsonConstructor] but it does nothing.

Example class with internal constructor:

public class Entry
{
    public int Id { get; }

    public string Name { get; set; }
    public string Value { get; set; }


    [JsonConstructor]
    internal Entry(int id, string name, string value)
    {
        this.Id = id;
        this.Name = name;
        this.Value = value;
    }
}

Suppose the user can use the library to get an Entry from the backend, modify its name and value, and update it. User should not be able to change the Id property or create a new Entry.

Example deserialization:

var json = "{\"id\": 1337, \"name\": \"TestEntry\", \"value\": \"TestValue\"}";
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);

var entry = JsonSerializer.Deserialize<Entry>(json, options);

Calling JsonSerializer.Deserialize will throw System.NotSupportedException - Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported:

A possible solution is to just make the constructor public and annotate it with [Obsolete("Message", true)] attribute - when trying to use this constructor the compiler will throw and error, making the constructor unusable. But I feel this is not the most elegant solution and it also misuses the [Obsolete] attribute.

Is there any other way of solving this problem?

Upvotes: 4

Views: 4343

Answers (2)

Shane
Shane

Reputation: 790

Had this when I ported an old Azure Function from .Net Core to .Net6.0 and started using the Microsoft.Azure.Functions.Worker instead of the .core Microsoft.Azure.WebJobs.Extensions.DurableTask files for the function Orchestrators.

when using Microsoft.DurableTask.Client Version="1.2.0" i got the same mentioned exception, when trying to de-serialise this passed class\object parameter

System.NotSupportedException - Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported:

IN ESSENSE

Microsoft.Extensions.Azure Version="1.5.0" seems to handle the de-serialisation fine even with unsupported types as per MS: Supported key Types

issue was when passing the detail object to the OrchestratorActivity possibly trying to de-serialize to FunctionContext for that method,

    var result = await context.CallActivityAsync<string>("EngineRun", detail);
    outputs.Add(result);
}

[Function("EngineRun")]
public async Task<string> EngineRun([ActivityTrigger] FunctionContext detail)

This is the detail object in question:

public class engineRun
{
    public string batchid { get; set; }
    public string taskid { get; set; }
    public string? saleid { get; set; }
    public int stage { get; set; }
    public ServiceClient? service { get; set; }
}

Hopes this helps - kept me busy for a while.

Edit to confirm even (braking all the rules) below still works and using System.Text.Json with Microsoft.Extensions.Azure :

[JsonPolymorphic]
public class engineRun
{
    private bool _started;
    [JsonConstructor]
    public engineRun() { }
    public engineRun(bool started)
    {
        _started = started;
    }
    public string batchid { get; set; }
    public string taskid { get; set; }
    public string? saleid { get; set; }
    public int stage { get; set; }
    public ServiceClient? service { get; set; }
    public bool Started
    {
        get { return _started; }
        private set { value = _started; }
    }
}

Upvotes: 1

Code Name Jack
Code Name Jack

Reputation: 3303

In this case the best way to achieve this would be by using a separate model for serialization. And then using an internal constructor on the Domain model.

public class EntryDto
{
    public int Id { get; }

    public string Name { get;  }
    public string Value { get; }


    [JsonConstructor]
    public EntryDto(int id, string name, string value)
    {
        this.Id = id;
        this.Name = name;
        this.Value = value;
    }
    
    public Entry ToDomainModel()
    {
      return new Entry(this.Id, this.Name, this.Value);
    }

}

public class Entry
{
    public int Id { get; }

    public string Name { get; }
    public string Value { get; }

    internal Entry(int id, string name, string value)
    {
        this.Id = id;
        this.Name = name;
        this.Value = value;
    }
}

```

Upvotes: 2

Related Questions