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:
Add a public parameterless constructor and make Repository
be mutable (i.e. add a setter for GitDirectory
), or
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.