英文:
How to implement custom JsonConverter based on property names?
问题
Here's the translated content without the code part:
我有一个转换器,像这样准备我的双精度数。
我想能够指定它已转换自哪个属性的名称(实际上是关于控制小数位数,但也可能需要其他内容)。
在覆盖的方法中没有找到任何好的方法或信息。我有两个想法,都不太吸引我。
- 通过属性在DTO类中为每个属性单独分配自定义转换器。我甚至不确定它是否会起作用,即使起作用,也太过繁琐。
- 在
JsonConverter<MyDto>
上实现转换器。这意味着需要大量不必要的自定义实现,而且还有繁琐的部分。
这些是唯一的选项吗,还有没有其他方法来解决这个问题?
英文:
I have a converter preparing my doubles like so.
class DoubleConverter : JsonConverter<double>
{
public override double Read(...) { ... }
public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options)
{
writer.WriteStringValue(value + " double from...?");
}
}
I'd like to be able to specify the name of the property it's been converted from. (In reality it's about controlling the number of digits but also other stuff may be needed).
Didn't really found any good approach (just followed the docs) or info in the overriden method. I have two ideas, non of which appeals to me.
- Assign the custom converter in the DTO class per each property individually using an attribute. I'm not even sure if it would work and even if so, it's waaaay to tedious.
- Implement the converter on
JsonConverter<MyDto>
. That would mean a lot of needless custom implementation of the obvious. And also, there's the tedious part.
Are those the only options or is there a way to scootch around it?
答案1
得分: 1
以下是您要求的翻译内容:
"Utf8JsonWriter"无法从中获取当前路径或属性名称的原因很简单,它不会跟踪路径。它唯一跟踪的是一个BitStack
,指示当前容器是数组还是对象。[1]
相反,在**.NET 7及更高版本**中,您可以使用typeInfo修饰符来自定义您类型的合同,以向每个双精度成员添加一个转换器,其构造函数将其MemberInfo
传递给它。
首先定义以下修饰符:
public static partial class JsonExtensions
{
public static Action<JsonTypeInfo> AddMemberAwareDoubleConverters { get; } = static typeInfo =>
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
return;
foreach (var property in typeInfo.Properties)
if (property.CustomConverter == null && property.GetMemberInfo() is {} memberInfo)
if (property.PropertyType == typeof(double))
property.CustomConverter = new MemberAwareDoubleConverter(memberInfo);
else if (property.PropertyType == typeof(double?))
property.CustomConverter = new MemberAwareNullableDoubleConverter(memberInfo);
};
public static MemberInfo? GetMemberInfo(this JsonPropertyInfo property) => (property.AttributeProvider as MemberInfo);
}
类MemberAwareDoubleConverter
:
class MemberAwareDoubleConverter : JsonConverter<double>
{
MemberInfo MemberInfo { get; }
public MemberAwareDoubleConverter(MemberInfo memberInfo) => this.MemberInfo = memberInfo ?? throw new ArgumentNullException(nameof(memberInfo));
public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) =>
writer.WriteRawValue($"{JsonSerializer.Serialize(value)} /* double from {MemberInfo.Name} */", true);
public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
// TODO: 处理 "NaN", "Infinity", "-Infinity"
reader.GetDouble();
}
类MemberAwareNullableDoubleConverter
:
class MemberAwareNullableDoubleConverter : JsonConverter<double?>
{
public override bool HandleNull => true;
MemberInfo MemberInfo { get; }
public MemberAwareNullableDoubleConverter(MemberInfo memberInfo) => this.MemberInfo = memberInfo ?? throw new ArgumentNullException(nameof(memberInfo));
public override void Write(Utf8JsonWriter writer, double? value, JsonSerializerOptions options) =>
writer.WriteRawValue($"{JsonSerializer.Serialize(value)} /* double? from {MemberInfo.Name} */", true);
public override double? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
// TODO: 处理 "NaN", "Infinity", "-Infinity"
reader.TokenType switch
{
JsonTokenType.Number => reader.GetDouble(),
JsonTokenType.Null => null,
_ => throw new JsonException(),
};
}
如果您的模型类似于以下示例:
public class Root
{
public double RootDoubleValue { get; set; }
public double? RootNullableValue { get; set; }
public List<Item> Items { get; set; } = new();
}
public record Item(double ItemDoubleValue, double? ItemNullableValue);
并且使用以下方式序列化:
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddMemberAwareDoubleConverters },
},
// 其他设置
WriteIndented = true,
ReadCommentHandling = JsonCommentHandling.Skip,
};
var json = JsonSerializer.Serialize(root, options);
您将看到生成的 JSON 包括注释,显示属性名称:
{
"RootDoubleValue": 101.01 /* double from RootDoubleValue */,
"RootNullableValue": 202.02 /* double? from RootNullableValue */,
"Items": [
{
"ItemDoubleValue": 2101.01 /* double from ItemDoubleValue */,
"ItemNullableValue": null /* double? from ItemNullableValue */
}
]
}
注意:
-
老实说,我不建议使用这种方法。根据属性名称自定义转换器的逻辑具有上帝对象代码味道,因为转换器需要知道应用程序中所有双精度属性的所有格式化规则。
相反,考虑向您的模型添加自定义属性,指示所需的格式,然后在您的
typeInfo
修饰符中将该信息传递给适当的转换器。或者,如果您只需控制小数位数,请从此答案中应用适当的
RoundingJsonConverter
到https://stackoverflow.com/q/71689776/3744182。
演示 fiddle 在这里。
[1] 您还不能从Utf8JsonReader
中获取路径。有关详细信息,请参阅https://stackoverflow.com/q/69136510。
英文:
It is not possible to get the the current path or property name from Utf8JsonWriter
for the simple reason that it does not track the path. The only thing it tracks is a BitStack
indicating whether the current container is an array or object. <sup>[1]</sup>
Instead, in .NET 7 and later you can use a typeInfo modifier to customize your type's contract to add a converter to each double-valued member whose constructor is passed its MemberInfo
.
First define the following modifier:
public static partial class JsonExtensions
{
public static Action<JsonTypeInfo> AddMemberAwareDoubleConverters { get; } = static typeInfo =>
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
return;
foreach (var property in typeInfo.Properties)
if (property.CustomConverter == null && property.GetMemberInfo() is {} memberInfo)
if (property.PropertyType == typeof(double))
property.CustomConverter = new MemberAwareDoubleConverter(memberInfo);
else if (property.PropertyType == typeof(double?))
property.CustomConverter = new MemberAwareNullableDoubleConverter(memberInfo);
};
public static MemberInfo? GetMemberInfo(this JsonPropertyInfo property) => (property.AttributeProvider as MemberInfo);
}
class MemberAwareDoubleConverter : JsonConverter<double>
{
MemberInfo MemberInfo { get; }
public MemberAwareDoubleConverter(MemberInfo memberInfo) => this.MemberInfo = memberInfo ?? throw new ArgumentNullException(nameof(memberInfo));
public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) =>
writer.WriteRawValue($"{JsonSerializer.Serialize(value)} /* double from {MemberInfo.Name} */", true);
public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
// TODO: Handle "NaN", "Infinity", "-Infinity"
reader.GetDouble();
}
class MemberAwareNullableDoubleConverter : JsonConverter<double?>
{
public override bool HandleNull => true;
MemberInfo MemberInfo { get; }
public MemberAwareNullableDoubleConverter(MemberInfo memberInfo) => this.MemberInfo = memberInfo ?? throw new ArgumentNullException(nameof(memberInfo));
public override void Write(Utf8JsonWriter writer, double? value, JsonSerializerOptions options) =>
writer.WriteRawValue($"{JsonSerializer.Serialize(value)} /* double? from {MemberInfo.Name} */", true);
public override double? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
// TODO: Handle "NaN", "Infinity", "-Infinity"
reader.TokenType switch
{
JsonTokenType.Number => reader.GetDouble(),
JsonTokenType.Null => null,
_ => throw new JsonException(),
};
}
Now if your model looks like, e.g.:
public class Root
{
public double RootDoubleValue { get; set; }
public double? RootNullableValue { get; set; }
public List<Item> Items { get; set; } = new ();
}
public record Item(double ItemDoubleValue, double? ItemNullableValue);
And you serialize using the modifier as follows:
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddMemberAwareDoubleConverters },
},
// Others as required
WriteIndented = true,
ReadCommentHandling = JsonCommentHandling.Skip,
};
var json = JsonSerializer.Serialize(root, options);
You will see that the JSON generated includes comments showing the property names:
{
"RootDoubleValue": 101.01 /* double from RootDoubleValue */,
"RootNullableValue": 202.02 /* double? from RootNullableValue */,
"Items": [
{
"ItemDoubleValue": 2101.01 /* double from ItemDoubleValue */,
"ItemNullableValue": null /* double? from ItemNullableValue */
}
]
}
Notes:
-
Honestly I don't recommend this approach. Customizing the logic of the converter based on the property name has a whiff of God object code smell because the converter is required to know all the formatting rules for all double-valued properties in your application.
Instead, consider adding custom attributes to your models indicating the required formatting, then in your
typeInfo
modifier, pass that information on to an appropriate converter.Or if you simply need to control the number of digits, apply an appropriate
RoundingJsonConverter
from this answer to https://stackoverflow.com/q/71689776/3744182.
Demo fiddle here.
<sup>[1]</sup> You also cannot get the path from Utf8JsonReader
. See https://stackoverflow.com/q/69136510 for details.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论