本文介绍了带有 System.Text.Json 的可选属性的自定义 JSON 序列化程序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧! 问题描述 限时删除!! 我正在尝试实现一个 JSON 序列化机制,它处理 null 和丢失的 JSON 值,以便能够在需要时执行部分更新(这样它就不会触及数据库中的字段当该值丢失时,但当该值显式设置为 null 时它会清除它).我创建了一个从 Roslyn 的 复制的自定义结构可选类型:public readonly struct Optional{public 可选(T值){this.HasValue = true;this.Value = 值;}public bool HasValue { get;}公共 T 值 { 得到;}公共静态隐式运算符 Optional T(T value) =>新的可选(值);公共覆盖字符串 ToS​​tring() =>this.HasValue ?(this.Value?.ToString() ?? null") : 未指定";}现在我希望能够对 JSON 进行序列化/反序列化,以便在通过 Optional<T> 对象往返时保留 JSON 中任何缺失的字段:public class CustomType{[JsonPropertyName("foo")]public 可选的富 { 得到;放;}[JsonPropertyName("bar")]public 可选的酒吧{得到;放;}[JsonPropertyName("baz")]public 可选的巴兹{得到;放;}}那么:var options = new JsonSerializerOptions();options.Converters.Add(new OptionalConverter());string json = @"{""foo"":0,""bar"":null}";自定义类型解析 = JsonSerializer.Deserialize(json, options);string roundtrippedJson = JsonSerializer.Serialize(parsed, options);//json 和 roundtrippedJson 应该是等价的Console.WriteLine("json:" + json);Console.WriteLine("roundtrippedJson:" + roundtrippedJson);我开始了一个基于 JsonConverterFactory 的实现,但如果可选的 HasValue 是 ,我似乎找不到在序列化过程中省略属性的正确方法错误:public class OptionalConverter : JsonConverterFactory{public override bool CanConvert(Type typeToConvert){if (!typeToConvert.IsGenericType) { return false;}if (typeToConvert.GetGenericTypeDefinition() != typeof(Optional<>)) { return false;}返回真;}公共覆盖 JsonConverter CreateConverter(类型 typeToConvert,JsonSerializerOptions 选项){类型 valueType = typeToConvert.GetGenericArguments()[0];返回 (JsonConverter)Activator.CreateInstance(类型:typeof(OptionalConverterInner).MakeGenericType(new Type[] { valueType }),bindingAttr: BindingFlags.Instance |BindingFlags.Public,活页夹:空,参数:空,文化:空);}私有类 OptionalConverterInner;:JsonConverter{公共覆盖 可选的<T>阅读(参考 Utf8JsonReader 阅读器,类型 typeToConvert,JsonSerializerOptions 选项){T 值 = JsonSerializer.Deserialize(参考阅读器,选项);返回新的可选(值);}public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options){//不起作用(产生无效的 JSON).//问题:此时对象的键已经写入 JSON 编写器中.如果 (value.HasValue){JsonSerializer.Serialize(writer, value.Value, options);}}}}问题:这会产生以下输出,这是无效的:json: {foo":0,bar":null}往返Json:{foo":0,bar":null,baz":}我该如何解决这个问题? 解决方案 自定义 JsonConverter 不能阻止转换器应用的值的序列化,请参阅 [System.Text.Json] 转换器级条件序列化 #36275 用于确认.在 .Net 5 中有一个选项可以忽略默认值,这可以满足您的需求,请参阅 如何使用 System.Text.Json 忽略属性.这个版本引入了 JsonIgnoreCondition.WhenWritingDefault:公共枚举 JsonIgnoreCondition{//在序列化或反序列化过程中永远不会忽略属性.从不 = 0,//在序列化和反序列化过程中始终忽略属性.总是 = 1,//如果值为默认值,则在序列化过程中忽略该属性.//这适用于引用和值类型的属性和字段.当WritingDefault = 2时,//如果值为空,则在序列化过程中忽略该属性.//这仅适用于引用类型的属性和字段.当WritingNull = 3 时,}您将能够通过 [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 或全局设置 JsonSerializerOptions.DefaultIgnoreCondition.因此,在 .Net 5 中,您的类将如下所示:公共类 CustomType{[JsonPropertyName("foo")][JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]public 可选的富 { 得到;放;}[JsonPropertyName("bar")][JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]public 可选的酒吧{得到;放;}[JsonPropertyName("baz")][JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]public 可选的巴兹{得到;放;}}并且应该从 OptionalConverterInner.Write() 中删除 HasValue 检查:public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) =>JsonSerializer.Serialize(writer, value.Value, options);演示小提琴 #1 此处.在 .Net 3 中,由于 System.Text.Json 中没有条件序列化机制,因此有条件地省略没有值的可选属性的唯一选择是编写一个 自定义 JsonConverter<T> 对于包含可选属性的所有类.这并不容易,因为 JsonSerializer 不提供任何访问到它的内部合约信息,所以我们需要为每个这样的类型手工制作一个转换器,或者通过反射编写我们自己的通用代码.这是创建此类通用代码的一种尝试:公共接口 IHasValue{bool HasValue { 得到;}对象 GetValue();}公共只读结构可选: IHasValue{public 可选(T值){this.HasValue = true;this.Value = 值;}public bool HasValue { get;}公共 T 值 { 得到;}公共对象 GetValue() =>价值;公共静态隐式运算符 Optional T(T value) =>新的可选(值);公共覆盖字符串 ToS​​tring() =>this.HasValue ?(this.Value?.ToString() ?? null") : 未指定";}公共类 TypeWithOptionalsConverter:JsonConverter<T>其中 T : 类, new(){类 TypeWithOptionalsConverterContractFactory : JsonObjectContractFactory{受保护的覆盖表达式 CreateSetterCastExpression(Expression e, Type t){//(Optional)(object)default(T) 不起作用,即使 (Optional)default(T) 起作用.//为了避免这个问题,我们需要先转换为 Nullable,然后转换为 Optionalif (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Optional<>))return Expression.Convert(Expression.Convert(e, t.GetGenericArguments()[0]), t);返回 base.CreateSetterCastExpression(e, t);}}静态只读 TypeWithOptionalsConverterContractFactory contractFactory = new TypeWithOptionalsConverterContractFactory();公共覆盖 T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options){var properties = contractFactory.GetProperties(typeToConvert);if (reader.TokenType == JsonTokenType.Null)返回空;if (reader.TokenType != JsonTokenType.StartObject)抛出新的 JsonException();var 值 = 新 T();而 (reader.Read()){if (reader.TokenType == JsonTokenType.EndObject)返回值;if (reader.TokenType != JsonTokenType.PropertyName)抛出新的 JsonException();string propertyName = reader.GetString();if (!properties.TryGetValue(propertyName, out var property) || property.SetValue == null){reader.Skip();}别的{var type = property.PropertyType.IsGenericType &&property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>)?property.PropertyType.GetGenericArguments()[0] : property.PropertyType;var item = JsonSerializer.Deserialize(ref reader, type, options);property.SetValue(value, item);}}抛出新的 JsonException();}public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options){writer.WriteStartObject();foreach(contractFactory.GetProperties(value.GetType()) 中的 var 属性){if (options.IgnoreReadOnlyProperties && property.Value.SetValue == null)继续;var item = property.Value.GetValue(value);如果(项目是 IHasValue hasValue){如果 (!hasValue.HasValue)继续;writer.WritePropertyName(property.Key);JsonSerializer.Serialize(writer, hasValue.GetValue(), options);}别的{if (options.IgnoreNullValues && item == null)继续;writer.WritePropertyName(property.Key);JsonSerializer.Serialize(writer, item, property.Value.PropertyType, options);}}writer.WriteEndObject();}}公共类 JsonPropertyContract{内部 JsonPropertyContract(PropertyInfo property, Func setterCastExpression){this.GetValue = ExpressionExtensions.GetPropertyFunc(property).Compile();if (property.GetSetMethod() != null)this.SetValue = ExpressionExtensions.SetPropertyFunc(property, setterCastExpression).Compile();this.PropertyType = property.PropertyType;}public Func<TBase, object>获取值 { 获取;}公共动作<TBase, object>设置值{获取;}公共类型 PropertyType { 获取;}}公共类 JsonObjectContractFactory{受保护的虚拟表达式 CreateSetterCastExpression(Expression e, Type t) =>Expression.Convert(e, t);ConcurrentDictionary>属性{获取;} =new ConcurrentDictionary>();ReadOnlyDictionary创建属性(类型类型){if (!typeof(TBase).IsAssignableFrom(type))抛出新的 ArgumentException();var 字典 = 类型.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy).Where(p => p.GetIndexParameters().Length == 0 && p.GetGetMethod() != null&&!Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute))).ToDictionary(p => p.GetCustomAttribute()?.Name ?? p.Name,p=>new JsonPropertyContract(p, (e, t) => CreateSetterCastExpression(e, t)),StringComparer.OrdinalIgnoreCase);返回字典.ToReadOnly();}public IReadOnlyDictionaryGetProperties(类型类型)=>Properties.GetOrAdd(type, t => CreateProperties(t));}公共静态类 DictionaryExtensions{public static ReadOnlyDictionaryToReadOnly(this IDictionary字典) =>new ReadOnlyDictionary(dictionary ?? throw new ArgumentNullException());}公共静态类 ExpressionExtensions{公共静态表达式<Func<T, object>>GetPropertyFunc(PropertyInfo 属性){//(x) =>(object)x.Property;var arg = Expression.Parameter(typeof(T), x");var getter = Expression.Property(arg, property);var cast = Expression.Convert(getter, typeof(object));return Expression.Lambda setterCastExpression){//(x, y) =>x.Property = (TProperty)yvar arg1 = Expression.Parameter(typeof(T), x");var arg2 = Expression.Parameter(typeof(object), y");var cast = setterCastExpression(arg2, property.PropertyType);var setter = Expression.Call(arg1, property.GetSetMethod(), cast);return Expression.Lambda注意事项:CustomType 仍然如您的问题所示.未尝试处理 JsonSerializerOptions.PropertyNamingPolicy.如有必要,您可以在 TypeWithOptionalsConverter 中实现这一点.我添加了一个非通用接口 IHasValue 以便在序列化期间更容易访问盒装 Optional.演示小提琴 #2 此处.或者,您可以坚持使用 Json.NET,它在属性和联系人级别支持此功能.见:根据其运行时值选择性地序列化一个属性(本质上是一个重复的你的问题).如何根据用户授权动态忽略json?I'm trying to implement a JSON serialization mechanism which handles both null and missing JSON values, to be able to perform partial updates when needed (so that it does not touch the field in the database when the value is missing, but it clears it when the value is explicitly set to null).I created a custom struct copied from Roslyn's Optional<T> type:public readonly struct Optional<T>{ public Optional(T value) { this.HasValue = true; this.Value = value; } public bool HasValue { get; } public T Value { get; } public static implicit operator Optional<T>(T value) => new Optional<T>(value); public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";}Now I want to be able to serialize/deserialize to/from JSON so that any missing field in JSON is preserved when roundtripping it through the Optional<T> object:public class CustomType{ [JsonPropertyName("foo")] public Optional<int?> Foo { get; set; } [JsonPropertyName("bar")] public Optional<int?> Bar { get; set; } [JsonPropertyName("baz")] public Optional<int?> Baz { get; set; }}Then:var options = new JsonSerializerOptions();options.Converters.Add(new OptionalConverter());string json = @"{""foo"":0,""bar"":null}";CustomType parsed = JsonSerializer.Deserialize<CustomType>(json, options);string roundtrippedJson = JsonSerializer.Serialize(parsed, options);// json and roundtrippedJson should be equivalentConsole.WriteLine("json: " + json);Console.WriteLine("roundtrippedJson: " + roundtrippedJson);I started an implementation based on JsonConverterFactory, but I can't seem to find a proper way to omit the property during serialization if the optional's HasValue is false:public class OptionalConverter : JsonConverterFactory{ public override bool CanConvert(Type typeToConvert) { if (!typeToConvert.IsGenericType) { return false; } if (typeToConvert.GetGenericTypeDefinition() != typeof(Optional<>)) { return false; } return true; } public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { Type valueType = typeToConvert.GetGenericArguments()[0]; return (JsonConverter)Activator.CreateInstance( type: typeof(OptionalConverterInner<>).MakeGenericType(new Type[] { valueType }), bindingAttr: BindingFlags.Instance | BindingFlags.Public, binder: null, args: null, culture: null ); } private class OptionalConverterInner<T> : JsonConverter<Optional<T>> { public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { T value = JsonSerializer.Deserialize<T>(ref reader, options); return new Optional<T>(value); } public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) { // Does not work (produces invalid JSON). // Problem: the object's key has already been written in the JSON writer at this point. if (value.HasValue) { JsonSerializer.Serialize(writer, value.Value, options); } } }}Problem: this produces the following output, which is invalid:json: {"foo":0,"bar":null}roundtrippedJson: {"foo":0,"bar":null,"baz":}How can I solve this? 解决方案 A custom JsonConverter<T> cannot prevent the serialization of a value to which the converter applies, see [System.Text.Json] Converter-level conditional serialization #36275 for confirmation.In .Net 5 there is an option to ignore default values which should do what you need, see How to ignore properties with System.Text.Json. This version introduces JsonIgnoreCondition.WhenWritingDefault:public enum JsonIgnoreCondition{ // Property is never ignored during serialization or deserialization. Never = 0, // Property is always ignored during serialization and deserialization. Always = 1, // If the value is the default, the property is ignored during serialization. // This is applied to both reference and value-type properties and fields. WhenWritingDefault = 2, // If the value is null, the property is ignored during serialization. // This is applied only to reference-type properties and fields. WhenWritingNull = 3,}You will be able to apply the condition to specific properties via [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] or globally by setting JsonSerializerOptions.DefaultIgnoreCondition.Thus in .Net 5 your class would look like:public class CustomType{ [JsonPropertyName("foo")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional<int?> Foo { get; set; } [JsonPropertyName("bar")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional<int?> Bar { get; set; } [JsonPropertyName("baz")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional<int?> Baz { get; set; }}And the HasValue check should be removed from OptionalConverterInner<T>.Write():public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value.Value, options);Demo fiddle #1 here.In .Net 3, as there is no conditional serialization mechanism in System.Text.Json, your only option to conditionally omit optional properties without a value is to write a custom JsonConverter<T> for all classes that contain optional properties. This is not made easy by the fact that JsonSerializer does not provide any access to its internal contract information so we need to either handcraft a converter for each and every such type, or write our own generic code via reflection.Here is one attempt to create such generic code:public interface IHasValue{ bool HasValue { get; } object GetValue();}public readonly struct Optional<T> : IHasValue{ public Optional(T value) { this.HasValue = true; this.Value = value; } public bool HasValue { get; } public T Value { get; } public object GetValue() => Value; public static implicit operator Optional<T>(T value) => new Optional<T>(value); public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";}public class TypeWithOptionalsConverter<T> : JsonConverter<T> where T : class, new(){ class TypeWithOptionalsConverterContractFactory : JsonObjectContractFactory<T> { protected override Expression CreateSetterCastExpression(Expression e, Type t) { // (Optional<Nullable<T>>)(object)default(T) does not work, even though (Optional<Nullable<T>>)default(T) does work. // To avoid the problem we need to first cast to Nullable<T>, then to Optional<Nullable<T>> if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Optional<>)) return Expression.Convert(Expression.Convert(e, t.GetGenericArguments()[0]), t); return base.CreateSetterCastExpression(e, t); } } static readonly TypeWithOptionalsConverterContractFactory contractFactory = new TypeWithOptionalsConverterContractFactory(); public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var properties = contractFactory.GetProperties(typeToConvert); if (reader.TokenType == JsonTokenType.Null) return null; if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); var value = new T(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) return value; if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException(); string propertyName = reader.GetString(); if (!properties.TryGetValue(propertyName, out var property) || property.SetValue == null) { reader.Skip(); } else { var type = property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>) ? property.PropertyType.GetGenericArguments()[0] : property.PropertyType; var item = JsonSerializer.Deserialize(ref reader, type, options); property.SetValue(value, item); } } throw new JsonException(); } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { writer.WriteStartObject(); foreach (var property in contractFactory.GetProperties(value.GetType())) { if (options.IgnoreReadOnlyProperties && property.Value.SetValue == null) continue; var item = property.Value.GetValue(value); if (item is IHasValue hasValue) { if (!hasValue.HasValue) continue; writer.WritePropertyName(property.Key); JsonSerializer.Serialize(writer, hasValue.GetValue(), options); } else { if (options.IgnoreNullValues && item == null) continue; writer.WritePropertyName(property.Key); JsonSerializer.Serialize(writer, item, property.Value.PropertyType, options); } } writer.WriteEndObject(); }}public class JsonPropertyContract<TBase>{ internal JsonPropertyContract(PropertyInfo property, Func<Expression, Type, Expression> setterCastExpression) { this.GetValue = ExpressionExtensions.GetPropertyFunc<TBase>(property).Compile(); if (property.GetSetMethod() != null) this.SetValue = ExpressionExtensions.SetPropertyFunc<TBase>(property, setterCastExpression).Compile(); this.PropertyType = property.PropertyType; } public Func<TBase, object> GetValue { get; } public Action<TBase, object> SetValue { get; } public Type PropertyType { get; }}public class JsonObjectContractFactory<TBase>{ protected virtual Expression CreateSetterCastExpression(Expression e, Type t) => Expression.Convert(e, t); ConcurrentDictionary<Type, ReadOnlyDictionary<string, JsonPropertyContract<TBase>>> Properties { get; } = new ConcurrentDictionary<Type, ReadOnlyDictionary<string, JsonPropertyContract<TBase>>>(); ReadOnlyDictionary<string, JsonPropertyContract<TBase>> CreateProperties(Type type) { if (!typeof(TBase).IsAssignableFrom(type)) throw new ArgumentException(); var dictionary = type .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) .Where(p => p.GetIndexParameters().Length == 0 && p.GetGetMethod() != null && !Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute))) .ToDictionary(p => p.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>()?.Name ?? p.Name, p => new JsonPropertyContract<TBase>(p, (e, t) => CreateSetterCastExpression(e, t)), StringComparer.OrdinalIgnoreCase); return dictionary.ToReadOnly(); } public IReadOnlyDictionary<string, JsonPropertyContract<TBase>> GetProperties(Type type) => Properties.GetOrAdd(type, t => CreateProperties(t));}public static class DictionaryExtensions{ public static ReadOnlyDictionary<TKey, TValue> ToReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> dictionary) => new ReadOnlyDictionary<TKey, TValue>(dictionary ?? throw new ArgumentNullException());}public static class ExpressionExtensions{ public static Expression<Func<T, object>> GetPropertyFunc<T>(PropertyInfo property) { // (x) => (object)x.Property; var arg = Expression.Parameter(typeof(T), "x"); var getter = Expression.Property(arg, property); var cast = Expression.Convert(getter, typeof(object)); return Expression.Lambda<Func<T, object>>(cast, arg); } public static Expression<Action<T, object>> SetPropertyFunc<T>(PropertyInfo property, Func<Expression, Type, Expression> setterCastExpression) { //(x, y) => x.Property = (TProperty)y var arg1 = Expression.Parameter(typeof(T), "x"); var arg2 = Expression.Parameter(typeof(object), "y"); var cast = setterCastExpression(arg2, property.PropertyType); var setter = Expression.Call(arg1, property.GetSetMethod(), cast); return Expression.Lambda<Action<T, object>>(setter, arg1, arg2); }}Notes:CustomType remains as shown in your question.No attempt was made to handle the presence of a naming policy in JsonSerializerOptions.PropertyNamingPolicy. You could implement this in TypeWithOptionalsConverter<T> if necessary.I added a non-generic interface IHasValue to enable easier access to a boxed Optional<T> during serialization.Demo fiddle #2 here.Alternatively, you could stick with Json.NET which supports this at the property and contact level. See:Optionally serialize a property based on its runtime value (essentially a duplicate of your question).how to dynamic jsonignore according to user authorize? 这篇关于带有 System.Text.Json 的可选属性的自定义 JSON 序列化程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持! 1403页,肝出来的..
09-06 17:07