gpt4 book ai didi

c# - 带有 System.Text.Json 的可选属性的自定义 JSON 序列化程序

转载 作者:行者123 更新时间:2023-12-03 13:32:36 25 4
gpt4 key购买 nike

我正在尝试实现一个 JSON 序列化机制来处理 null和丢失的 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 的实现, 但如果可选的 HasValuefalse :
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 中有一个选项可以忽略默认值,这应该可以满足您的需要,请参见 How to ignore properties with System.Text.Json 。这个版本引入了 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,
}

您将能够通过 [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 或通过设置 JsonSerializerOptions.DefaultIgnoreCondition 将条件应用于特定属性。
因此,在 .Net 5 中,您的类(class)将如下所示:
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; }
}
并且应该从 HasValue 中删除 OptionalConverterInner<T>.Write() 检查:
public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, value.Value, options);
演示 fiddle #1 here
在 .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, 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);
}
}
笔记:
  • CustomType 仍然如您的问题所示。
  • 未尝试处理 JsonSerializerOptions.PropertyNamingPolicy 中存在的命名策略。如有必要,您可以在 TypeWithOptionalsConverter<T> 中实现这一点。
  • 我添加了一个非通用接口(interface) IHasValue 以便在序列化期间更轻松地访问装箱的 Optional<T>

  • 演示 fiddle #2 here
    或者 ,您可以坚持使用在属性和联系人级别支持此功能的 Json.NET。看:
  • Optionally serialize a property based on its runtime value (基本上是您问题的副本)。
  • how to dynamic jsonignore according to user authorize?
  • 关于c# - 带有 System.Text.Json 的可选属性的自定义 JSON 序列化程序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63418549/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com