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

人气:446 发布:2022-10-16 标签: json constructor .net-5 system.text.json

问题描述

给定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时,会引发以下异常:

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的已知限制。请参阅问题:

[JsonSerializer] Relax restrictions on ctor param type to immutable property type matching where reasonable #44428,当前标记为6.0.0里程碑。 System.Text.Json incorrectly requires construct parameter types to match immutable property types. #47422 JsonConstrutor different behavior between Newtonsoft.Json and System.Text.Json #46480。

因此(至少在.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--但不能同时--可以是非公共的。(我不知道为什么Microsoft不允许两者都是私有的,数据协定序列化程序肯定支持标记为[DataMember]的私有成员。)在本例中,我选择将getter设置为私有,以减少其他序列化程序序列化或通过属性浏览器显示的机会。

演示小提琴#1here。

或者,您可以为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);
}

对于简单模型,这似乎是更容易的解决方案,但对于复杂模型或经常向其添加属性的模型来说,这可能会成为一种麻烦。

演示小提琴#2here。

最后,似乎有点遗憾的是,Data属性在反序列化过程中会预先考虑一些在序列化过程中不存在的额外标头。与其在反序列化期间修复此问题,不如考虑修改您的体系结构,以避免从一开始就损坏Data字符串。

527