问题描述
我正在尝试实现一种同时处理null
和缺少的JSON值的JSON序列化机制,以便能够在需要时执行部分更新(以使该值不触及数据库中的字段)丢失,但在将值显式设置为null
时将其清除。
我创建了一个自定义结构,该结构是从Roslyn的Optional<T>
类型复制的:
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";
}
现在,我希望能够从JSON序列化/反序列化,以便在通过Optional<T>
对象进行往返处理时保留JSON中所有丢失的字段:
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; }
}
然后:
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 equivalent
Console.WriteLine("json: " + json);
Console.WriteLine("roundtrippedJson: " + roundtrippedJson);
我基于JsonConverterFactory
开始了一个实现,但是如果可选的HasValue
为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);
}
}
}
}
问题:这将产生以下输出,但无效:
json: {"foo":0,"bar":null}
roundtrippedJson: {"foo":0,"bar":null,"baz":}
我该如何解决?
解决方法
自定义JsonConverter<T>
不能阻止转换器应用的值的序列化,请参见 [System.Text.Json] Converter-level conditional serialization #36275 进行确认。
在.Net 5中,将有一个选项可以忽略默认值,该默认值应满足您的需要,请参见已关闭的 System.Text.Json option to ignore default values during serialization #779 在.Net 5中实现。此版本引入了JsonIgnoreCondition.WhenWritingDefault
:
public enum JsonIgnoreCondition { /// <summary> /// Property is never ignored during serialization or deserialization. /// </summary> Never = 0,/// <summary> /// Property is always ignored during serialization and deserialization. /// </summary> Always = 1,/// <summary> /// If the value is the default,the property is ignored during serialization. /// This is applied to both reference and value-type properties and fields. /// </summary> WhenWritingDefault = 2,/// <summary> /// If the value is <see langword="null"/>,the property is ignored during serialization. /// This is applied only to reference-type properties and fields. /// </summary> WhenWritingNull = 3,}
您将能够通过[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
或通过设置JsonSerializerOptions.DefaultIgnoreCondition
将条件应用于特定属性。
因此,在.Net 5中,您的类如下所示:
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; }
}
(。Net 5当然仍处于预览状态,因此最终的API可能会更改。)
在.Net 3中,由于System.Text.Json
中没有条件序列化机制,因此,有条件地省略没有值的可选属性的唯一选择是写一个custom JsonConverter<T>
用于所有包含可选属性的类。 JsonSerializer
does not provide any access to its internal contract information的事实使这一点变得不容易,因此我们需要为每种此类类型手工制作一个转换器,或者通过反射编写我们自己的通用代码。
这是创建此类通用代码的一种尝试:
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,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,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);
}
}
注意:
-
CustomType
仍然显示在您的问题中。 -
未尝试处理
JsonSerializerOptions.PropertyNamingPolicy
中的命名策略。如有必要,您可以在TypeWithOptionalsConverter<T>
中实现。 -
我添加了一个非通用接口
IHasValue
,以便在序列化期间更轻松地访问盒装Optional<T>
。
演示小提琴here。
或者,您可以坚持使用Json.NET,它在属性和联系人级别支持此功能。参见: