27

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?

3
  • 1
    You need change constructor of class CustomGit.Repository (as described in error) or create parameterless constructor for it Commented Apr 25, 2022 at 14:13
  • 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? Commented Apr 25, 2022 at 14:27
  • 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. Commented Apr 25, 2022 at 14:40

2 Answers 2

33

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.

Sign up to request clarification or add additional context in comments.

2 Comments

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 constructor
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; }"
0

In my case I was inheriting from an abstract class. This class had an empty constructor and so did my child class. I even tried a custom JsonConverter for my data object class I was passing to my controller, which was receiving it using the [FromBody] annotation as a parameter.

That did not help.. my problem was my abstract class did not have { get; set; } on its public properties. I created this class with a parameterized constructor to avoid it being an anemic domain model, but did not think it would need the getters and setters. Be sure the JSON Property names (if you have attributes) also match. I spent hours on this one!

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.