ASP.NET Core - System.Text.Json:如何拒绝有效负载中的未知属性?

sil*_*ent 7 c# asp.net-core system.text.json

ASP.NET Core 7 中的 Web API 与 System.Text.Json:

我需要拒绝 PUT/POST API 上的 JSON 有效负载,这些 API 指定了其他属性,这些属性不映射到模型中的任何属性。

所以如果我的模型是

public class Person {
  public string Name { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我需要拒绝任何如下所示的有效负载(带有 400-Bad Request 错误)

{
  "name": "alice",
  "lastname": "bob"
}
Run Code Online (Sandbox Code Playgroud)

如何才能实现这一目标?

dbc*_*dbc 5

目前,System.Text.Json 没有与 Json.NETMissingMemberHandling.Error功能等效的选项,可以在反序列化的 JSON 具有未映射的属性时强制出现错误。如需确认,请参阅:

然而,尽管官方文档指出缺少成员功能没有解决方法,但您可以使用该[JsonExtensionData]属性来模拟MissingMemberHandling.Error.

首先,如果您只想实现几种类型,您可以添加一个扩展数据字典,然后检查它是否包含内容并在回调中或在控制器中MissingMemberHandling.Error抛出异常,如Michael Liu回答所建议的。JsonOnDeserialized.OnDeserialized()

其次,如果您需要为每种类型实现MissingMemberHandling.Error,在 .NET 7 及更高版本中,您可以添加一个DefaultJsonTypeInfoResolver 修饰符,该修饰符添加一个合成扩展数据属性,该属性会在未知属性上引发错误。

为此,请定义以下扩展方法:

public static class JsonExtensions
{
    public static DefaultJsonTypeInfoResolver AddMissingMemberHandlingError(this DefaultJsonTypeInfoResolver resolver)
    {
        resolver.Modifiers.Add(typeInfo => 
                               {
                                   if (typeInfo.Kind != JsonTypeInfoKind.Object)
                                       return;
                                   if (typeInfo.Properties.Any(p => p.IsExtensionData))
                                       return;
                                   var property = typeInfo.CreateJsonPropertyInfo(typeof(Dictionary<string, JsonElement>), "<>ExtensionData");
                                   property.IsExtensionData = true;
                                   property.Get = static (obj) => null;
                                   property.Set = static (obj, val) => 
                                   {
                                       var dictionary = (Dictionary<string, JsonElement>?)val;
                                       Console.WriteLine(dictionary?.Count);
                                       if (dictionary != null)
                                           throw new JsonException();
                                   };
                                   typeInfo.Properties.Add(property);
                               });
        return resolver;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后按如下方式配置您的选项:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
        .AddMissingMemberHandlingError(),
};
Run Code Online (Sandbox Code Playgroud)

完成此操作后,JsonException当遇到缺少的 JSON 属性时,将会抛出 a 。但请注意,Systen.Text.Json 在填充之前设置分配的字典,因此在使用此解决方法时,您将无法在异常消息中包含缺少的成员名称。

演示小提琴在这里

更新

MissingMemberHandling.Error如果您需要为每种类型实现,并且还需要异常错误消息来包含未知属性的名称,则可以通过定义自定义字典类型来完成,每当尝试向字典添加任何内容时,该类型都会引发自定义异常。然后使用该自定义字典类型作为合约修饰符添加的合成扩展属性中的扩展字典类型,如下所示:

// A JsonException subclass that allows for a custom message that includes the path, line number and byte position.
public class JsonMissingMemberException : JsonException
{
    readonly string? innerMessage;
    public JsonMissingMemberException() : this(null) { }
    public JsonMissingMemberException(string? innerMessage) : base(innerMessage) => this.innerMessage = innerMessage;
    public JsonMissingMemberException(string? innerMessage, Exception? innerException) : base(innerMessage, innerException) => this.innerMessage = innerMessage;
    protected JsonMissingMemberException(SerializationInfo info, StreamingContext context) : base(info, context) => this.innerMessage = (string?)info.GetValue("innerMessage", typeof(string));
    public override string Message =>
        innerMessage == null
            ? base.Message
            : String.Format("{0} Path: {1} | LineNumber: {2} | BytePositionInLine: {3}.", innerMessage, Path, LineNumber, BytePositionInLine);
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);
        info.AddValue("innerMessage", innerMessage);
    }
}

public static class JsonExtensions
{
    class UnknownPropertyDictionary<TModel> : IDictionary<string, JsonElement>
    {       
        static JsonException CreateException(string key, JsonElement value) =>
            new JsonMissingMemberException(String.Format("Unexpected property \"{0}\" encountered while deserializing type {1}.", key, typeof(TModel).FullName));
        
        public void Add(string key, JsonElement value) => throw CreateException(key, value);
        public bool ContainsKey(string key) => false;
        public ICollection<string> Keys => Array.Empty<string>();
        public bool Remove(string key) => false; 
                                    
        public bool TryGetValue(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out JsonElement value) { value = default; return false; }
        public ICollection<JsonElement> Values => Array.Empty<JsonElement>();
        public JsonElement this[string key]
        {
            get => throw new KeyNotFoundException(key);
            set =>  throw CreateException(key, value);
        }
        public void Add(KeyValuePair<string, JsonElement> item) =>  throw CreateException(item.Key, item.Value);
        public void Clear() => throw new NotImplementedException();
        public bool Contains(KeyValuePair<string, JsonElement> item) => false;
        public void CopyTo(KeyValuePair<string, JsonElement>[] array, int arrayIndex) { }
        public int Count => 0;
        public bool IsReadOnly => false;
        public bool Remove(KeyValuePair<string, JsonElement> item) => false;
        public IEnumerator<KeyValuePair<string, JsonElement>> GetEnumerator() => Enumerable.Empty<KeyValuePair<string, JsonElement>>().GetEnumerator();
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
    }

    public static DefaultJsonTypeInfoResolver AddMissingMemberHandlingError(this DefaultJsonTypeInfoResolver resolver)
    {
        resolver.Modifiers.Add(typeInfo => 
                               {
                                   if (typeInfo.Kind != JsonTypeInfoKind.Object)
                                       return;
                                   if (typeInfo.Properties.Any(p => p.IsExtensionData))
                                       return;
                                   var dictionaryType = typeof(UnknownPropertyDictionary<>).MakeGenericType(typeInfo.Type);
                                   JsonPropertyInfo property = typeInfo.CreateJsonPropertyInfo(dictionaryType, "<>ExtensionData");
                                   property.IsExtensionData = true;
                                   property.Get = (obj) => Activator.CreateInstance(dictionaryType);
                                   property.Set = static (obj, val) => { };
                                   typeInfo.Properties.Add(property);
                               });
        return resolver;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,如果我尝试将具有未知属性的 JSON 反序列化为不包含该属性的模型,则会引发以下异常:

public static class JsonExtensions
{
    public static DefaultJsonTypeInfoResolver AddMissingMemberHandlingError(this DefaultJsonTypeInfoResolver resolver)
    {
        resolver.Modifiers.Add(typeInfo => 
                               {
                                   if (typeInfo.Kind != JsonTypeInfoKind.Object)
                                       return;
                                   if (typeInfo.Properties.Any(p => p.IsExtensionData))
                                       return;
                                   var property = typeInfo.CreateJsonPropertyInfo(typeof(Dictionary<string, JsonElement>), "<>ExtensionData");
                                   property.IsExtensionData = true;
                                   property.Get = static (obj) => null;
                                   property.Set = static (obj, val) => 
                                   {
                                       var dictionary = (Dictionary<string, JsonElement>?)val;
                                       Console.WriteLine(dictionary?.Count);
                                       if (dictionary != null)
                                           throw new JsonException();
                                   };
                                   typeInfo.Properties.Add(property);
                               });
        return resolver;
    }
}
Run Code Online (Sandbox Code Playgroud)

笔记:

  • JsonException需要一个自定义子类来包含自定义消息以及路径、行号和字节位置。

  • 异常消息中仅包含第一个未知属性的名称。

演示 #2在这里