Home > Enterprise >  ASP.NET Core - System.Text.Json: how to reject unknown properties in payload?
ASP.NET Core - System.Text.Json: how to reject unknown properties in payload?

Time:01-04

Web API in ASP.NET Core 7 with System.Text.Json:

I need to reject JSON payloads on PUT/POST APIs which have additional properties specified which do not map to any properties in the model.

So if my model is

public class Person {
  public string Name { get; set; }
}

I need to reject any payloads (with a 400-Bad Request error) which look like this

{
  "name": "alice",
  "lastname": "bob"
}

How can this be achieved?

CodePudding user response:

If you're willing to pollute your model class, you can add an extension data property that collects all extraneous properties in the payload:

public class Person {
    public string Name { get; set; }

    [JsonExtensionData] // using System.Text.Json.Serialization;
    public IDictionary<string, JsonElement> ExtensionData { get; set; }
}

Then in your controller, check whether person.ExtensionData is non-null:

if (person.ExtensionData != null) {
    return BadRequest();
}

If you have numerous model classes and controllers, I'd define an interface for the ExtensionData property that each model class implements, and install a global filter that validates the ExtensionData property.

CodePudding user response:

Currently System.Text.Json does not have an option equivalent to Json.NET's MissingMemberHandling.Error functionality to force an error when the JSON being deserialized has an unmapped property. For confirmation, see:

However, even though the official documentation states that there's no workaround for the missing member feature, you can make use of the the [JsonExtensionData] attribute to emulate MissingMemberHandling.Error.

Firstly, if you only have a few types for which you want to implement MissingMemberHandling.Error, you could add an extension data dictionary then check whether it contains contents and throw an exception in an JsonOnDeserialized.OnDeserialized() callback, or in your controller as suggested by this answer by Michael Liu.

Secondly, if you need to implement MissingMemberHandling.Error for every type, in .NET 7 and later you could add a DefaultJsonTypeInfoResolver modifier that adds a synthetic extension data property that throws an error on an unknown property.

To do this, define the following extension method:

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;
    }
}

And then configure your options as follows:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
        .AddMissingMemberHandlingError(),
};
        

Having done so, a JsonException will be thrown when an missing JSON property is encountered. Note however that Systen.Text.Json sets the allocated dictionary before it is populated, so you won't be able to include the missing member name in the exception message when using this workaround.

Demo fiddle here.

  • Related