Each parameter in the deserialization constructor on type must bind to an object property or field on deserialization
Asked Answered
H

1

19

I have the following simple classes :

public abstract class GitObject
{
    public Repository Repository { get; set; }
    public abstract string Serialize();
    public abstract void Deserialize(string data);

    public class Blob : GitObject
    {
        public string Data { get; set; }

        public Blob(Repository repository, string data = null)
        {
            if (data != null) Data = File.ReadAllText(data);
            Repository = repository;
        }
        public override string Serialize()
        {
            return JsonSerializer.Serialize(this);
        }
        public override void Deserialize(string data)
        {
            Blob blobData = JsonSerializer.Deserialize<Blob>(data);
        }
    }
}

I know there is probably a LOT of room for improvement ( and I a am happy to hear about it ). However, the method Deserialize gives me the error

Each parameter in the deserialization constructor on type 'CustomGit.Repository' 
must bind to an object property or field on deserialization. Each parameter name must
match with a property or field on the object. The match can be case-insensitive.

For testing if this method works as intended I use this approach (which also throws the error)

FileInfo file = new FileInfo(Path.Combine(repository.GitDirectory.FullName, "code.txt"));

GitObject.Blob firstBlob = new GitObject.Blob(repository, file.FullName);
var json = firstBlob.Serialize();

GitObject.Blob secondBlob = new GitObject.Blob(repository);
secondBlob.Deserialize(json);

What am I doing wrong and what should I change in general?

Hiatt answered 25/4, 2022 at 14:9 Comment(3)
You need change constructor of class CustomGit.Repository (as described in error) or create parameterless constructor for itHotspur
I see, but what exactly is causing the issue? Does the Deserialize method need all properties and / or fields of every class that is "assigned" to the object to be deserialized?Hiatt
On deserialization object must be created and if constructor does not contains all properties and/or fields it does not good (at least). If we talking about newtonsoftjson, as i remember you can mark constructor with JsonConstructorAttribute, it can help.Hotspur
V
27

You are encountering two separate problems related to deserializing types with parameterized constructors. As explained in the documentation page How to use immutable types and non-public accessors with System.Text.Json:

System.Text.Json can use a public parameterized constructor, which makes it possible to deserialize an immutable class or struct. For a class, if the only constructor is a parameterized one, that constructor will be used. For a struct, or a class with multiple constructors, specify the one to use by applying the [JsonConstructor] attribute. When the attribute is not used, a public parameterless constructor is always used if present. The attribute can only be used with public constructors.

...

The parameter names of a parameterized constructor must match the property names and types. Matching is case-insensitive, and the constructor parameter must match the actual property name even if you use [JsonPropertyName] to rename a property. [1]

Your first problem is with the type Repository. You don't show it in your question, but I assume it looks something like this:

public class Repository
{
    public Repository(string gitDirectory) => this.GitDirectory = new DirectoryInfo(gitDirectory);

    [JsonConverter(typeof(DirectoryInfoConverter))]
    public DirectoryInfo GitDirectory { get; }
}

public class DirectoryInfoConverter : JsonConverter<DirectoryInfo>
{
    public override DirectoryInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
        new DirectoryInfo(reader.GetString());
    public override void Write(Utf8JsonWriter writer, DirectoryInfo value, JsonSerializerOptions options) =>
        writer.WriteStringValue(value.ToString());
}

If so, your problem here is that either the name of the constructor argument corresponding to GitDirectory is not the same as the property name or the type of the argument is not the same.

Demo fiddle #1 here.

To fix this, you must either:

  1. Add a public parameterless constructor and make Repository be mutable (i.e. add a setter for GitDirectory), or

  2. Add a constructor with an argument of the same type and name as the property GitDirectory, and mark it with [JsonConstructor].

Adopting option #2, your Repository type should now look like:

public class Repository
{
    public Repository(string gitDirectory) => this.GitDirectory = new DirectoryInfo(gitDirectory);
    [JsonConstructor]
    public Repository(DirectoryInfo gitDirectory) => this.GitDirectory = gitDirectory ?? throw new ArgumentNullException(nameof(gitDirectory));

    [JsonConverter(typeof(DirectoryInfoConverter))]
    public DirectoryInfo GitDirectory { get; }
}

And now Respository will deserialize successfully. Demo fiddle #2 here.

However, you will now encounter your second problem, namely that the Blob type will not round-trip either. In this case, Blob does have a unique parameterized constructor whose argument names and types correspond precisely to properties -- but the semantics of one of them, data, are completely different:

public class Blob : GitObject
{
    public string Data { get; set; }

    public Blob(Repository repository, string data = null)
    {
        if (data != null) 
            Data = File.ReadAllText(data);
        Repository = repository;
    }

The property Data corresponds to the textual contents of a file, while the argument data corresponds to the file name of a file. Thus when deserializing Blob your code will attempt to read a file whose name equals the file's contents, and fail.

This inconsistency is, in my opinion, poor programming style, and likely to confuse other developers as well as System.Text.Json. Instead, consider adding factory methods to create a Blob from a file, or from file contents, and remove the corresponding constructor argument. Thus your Blob should look like:

public class Blob : GitObject
{
    public string Data { get; set; }

    public Blob(Repository repository) => this.Repository = repository ?? throw new ArgumentNullException(nameof(repository));

    public static Blob CreateFromDataFile(Repository repository, string dataFileName) =>
        new Blob(repository)
        {
            Data = File.ReadAllText(dataFileName),
        };
    
    public static Blob CreateFromDataConents(Repository repository, string data) =>
        new Blob(repository)
        {
            Data = data,
        };
    
    public override string Serialize() => JsonSerializer.Serialize(this);

    public override void Deserialize(string data)
    {
        // System.Text.Json does not have a Populate() method so we have to do it manually, or via a tool like AutoMapper
        Blob blobData = JsonSerializer.Deserialize<Blob>(data);
        this.Repository = blobData.Repository;
        this.Data = blobData.Data;
    }
}

And you would construct and round-trip it as follows:

var firstBlob = GitObject.Blob.CreateFromDataFile(repository, file.FullName);
var json = firstBlob.Serialize();

var secondBlob = new GitObject.Blob(repository);
secondBlob.Deserialize(json);           

Final working demo fiddle here.


[1] The documentation was updated in 2023. At the time this question was asked, the documentation merely stated

The parameter names of a parameterized constructor must match the property names.

Vaccination answered 5/6, 2022 at 19:23 Comment(2)
The part in bold - "the type of the argument is not the same" - did it for me. I had an object in my contract so that it could be serialized using polymorphism, but I had a type restraint in the actual constructorCabbage
My problem was my class only had fields, but it needed to have properties instead. For instance, I replaced "public string name;" with "public string name { get; set; }"Appositive

© 2022 - 2024 — McMap. All rights reserved.