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

问题描述 投票:0回答:2

给定 Base64 字符串,以下示例类将使用 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,会引发以下异常:

必须在反序列化时绑定到对象属性或字段。每个参数名称必须与对象上的属性或字段匹配。匹配可以不区分大小写。

显然 System.Text.Json 不喜欢属性是 Byte[] 但参数是 String,这并不重要,因为重点是构造函数应该负责赋值。

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

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

json constructor system.text.json .net-5
2个回答
10
投票

这显然是

System.Text.Json
的已知限制。 查看问题:

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

一种解决方案是添加代理 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
只会序列化公共属性。 但是,如果您使用
[JsonInclude]
标记属性,那么 setter getter —— 但不能同时是两者 —— 都可以是非公开的。 (我不知道为什么微软不允许两者都是私有的,数据契约序列化程序当然支持标有
[DataMember]
的私有成员。)在这种情况下,我选择将 getter 设为私有以减少代理属性被序列化的机会由其他序列化程序或通过某些属性浏览器显示。

演示小提琴 #1 这里

或者,您可以为 JsonConverter<T>

 引入 自定义 
AvatarImage

[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);
}

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

演示小提琴 #2 这里

最后,似乎有点不幸的是,

Data
属性在反序列化期间会预先添加一些在序列化期间不存在的额外标头。 不要在反序列化期间修复此问题,而是考虑修改架构以避免首先损坏
Data
字符串。


0
投票

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

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 不需要随着基类被反序列化而不断增长。它还使得处理潜在的可为 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))
  };
}
© www.soinside.com 2019 - 2024. All rights reserved.