System.Text.Json(但不是 Newtonsoft.Json)中的 JsonConstructorAttribute 在 属性 和构造函数参数类型不同时导致异常

JsonConstructorAttribute in System.Text.Json (but not Newtonsoft.Json) results in exception when property and constructor argument types differ

给定 Base64 字符串,以下示例 class 将使用 Newtonsoft.Json 正确反序列化,但不会使用 System.Text.Json:

using System;
using System.Text.Json.Serialization;

public class AvatarImage{

  public Byte[] Data { get; set; } = null;

  public AvatarImage() {
  }

  [JsonConstructor]
  public AvatarImage(String Data) {
  //Remove Base64 header info, leaving only the data block and convert it to a Byte array
    this.Data = Convert.FromBase64String(Data.Remove(0, Data.IndexOf(',') + 1));
  }

}

使用System.Text.Json,抛出以下异常:

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.

显然 System.Text.Json 不喜欢 属性 是一个 Byte[] 但参数是一个字符串,这并不重要,因为重点是构造函数应该做好作业。

有什么方法可以使它与 System.Text.Json 一起使用吗?

在我的特殊情况下,Base64 图像被发送到 WebAPI 控制器,但最终对象只需要 Byte[]。在 Newtonsoft 中,这是一个快速而干净的解决方案。

这显然是 System.Text.Json 的已知限制。查看问题:

因此(至少在 .Net 5 中)您需要重构 class 以避免限制。

一种解决方案是添加代理项 Base64 编码字符串 属性:

public class AvatarImage
{
    [JsonIgnore]
    public Byte[] Data { get; set; } = null;

    [JsonInclude]
    [JsonPropertyName("Data")]
    public string Base64Data 
    { 
        private get => Data == null ? null : Convert.ToBase64String(Data);
        set
        {
            var index = value.IndexOf(',');
            this.Data = Convert.FromBase64String(index < 0 ? value : value.Remove(0, index + 1));
        }
    }
}

请注意,通常 JsonSerializer 只会序列化 public 属性。但是,如果您将 属性 标记为 [JsonInclude],则 或者 setter getter -- 但不是两者都 -- 可以是非public。 (我不知道为什么 Microsoft 不允许两者都是私有的,数据契约序列化器当然支持用 [DataMember] 标记的私有成员。)在这种情况下,我选择将 getter 设为私有以减少代理 属性 可能被其他序列化程序序列化或通过某些 属性 浏览器显示。

演示 fiddle #1 here.

或者,您可以为 AvatarImage

引入 custom JsonConverter<T>
[JsonConverter(typeof(AvatarConverter))]
public class AvatarImage
{
    public Byte[] Data { get; set; } = null;
}

class AvatarConverter : JsonConverter<AvatarImage>
{
    class AvatarDTO { public string Data { get; set; } }
    public override AvatarImage Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var dto = JsonSerializer.Deserialize<AvatarDTO>(ref reader, options);
        var index = dto.Data?.IndexOf(',') ?? -1;
        return new AvatarImage { Data = dto.Data == null ? null : Convert.FromBase64String(index < 0 ? dto.Data : dto.Data.Remove(0, index + 1)) };
    }

    public override void Write(Utf8JsonWriter writer, AvatarImage value, JsonSerializerOptions options) =>
        JsonSerializer.Serialize(writer, new { Data = value.Data }, options);
}

对于简单的模型,这似乎是更简单的解决方案,但对于复杂的模型或经常添加属性的模型来说可能会很麻烦。

演示 fiddle #2 here.

最后,似乎有点不幸的是 Data 属性 在反序列化期间会有一些额外的 header 在序列化期间不存在。与其在反序列化期间修复此问题,不如考虑修改您的体系结构以避免首先破坏 Data 字符串。

如果需要,使用 ExpandoObject 实现自定义转换器反序列化可以避免嵌套 DTO class:

using System.Dynamic;

.
.
.

public override FileEntity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
  dynamic obj = JsonSerializer.Deserialize<ExpandoObject>(ref reader, options);
  return new FileEntity {
    Data = (obj.data == null) ? null : Convert.FromBase64String(obj.data.GetString().Remove(0, obj.data.GetString().IndexOf(',') + 1))
  };
}

它使自定义转换器在开发时更加灵活,因为 DTO 不需要随着基 class 反序列化而不断增长。它还使处理潜在的可为 null 的属性也更容易一些(通过标准 JsonElement 反序列化,即 JsonSerializer.Deserialize< JsonElement >),如下所示:

public override FileEntity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
  dynamic obj = JsonSerializer.Deserialize<ExpandoObject>(ref reader, options);
  return new FileEntity {
    SomeNullableInt32Property = obj.id?.GetInt32(),
    Data = (obj.data?.GetString() == null) ? null : Convert.FromBase64String(obj.data.GetString().Remove(0, obj.data.GetString().IndexOf(',') + 1))
  };
}