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
的已知限制。查看问题:
- [JsonSerializer] Relax restrictions on ctor param type to immutable property type matching where reasonable #44428 which is currently labeled with the 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 中)您需要重构 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))
};
}
给定 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
的已知限制。查看问题:
- [JsonSerializer] Relax restrictions on ctor param type to immutable property type matching where reasonable #44428 which is currently labeled with the 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 中)您需要重构 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
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))
};
}