runtime: Add support for MissingMemberHandling to System.Text.Json

We would love to migrate our ASP.NET Core 3 application from using Newtonsoft.Json to System.Text.Json. However, the lack of support/workaround for MissingMemberHandling is a showstopper, because we need a robust way to detect and report deficient [FromBody] model binding. Without this feature, mistakes in the input JSON document can go undetected, and that is not acceptable.

Is adding MissingMemberHandling something that is planned for System.Text.Json?

EDIT @eiriktsarpalis: API proposal & usage examples added

API Proposal

namespace System.Text.Json;

public enum JsonMissingMemberHandling { Ignore = 0, Error = 1 }

public partial class JsonSerializerOptions
{
     // Global configuration
     public JsonMissingMemberHandling MissingMemberHandling { get; set; } = JsonMissingMemberHandling.Ignore;
}
namespace System.Text.Json.Serialization;

// Per-type attribute-level configuration
[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple=false, Inherited=false)]
public class JsonMissingMemberHandlingAttribute : JsonAttribute
{
       public JsonMissingMemberHandlingAttribute(JsonMissingMemberHandling missingMemberHandling);
       public JsonMissingMemberHandlingAttribute MissingMemberHandling { get; }
}

namespace System.Text.Json.Serialization.Metadata;

public partial class JsonTypeInfo
{
      // Per-type configuration via contract customization.
      public JsonMissingMemberHandling MissingMemberHandling { get; set; } = JsonMissingMemberHandling.Ignore;
}

API Usage

JsonSerializer.Deserialize<MyPoco>("""{ "jsonPropertyNotBindingToPocoProperty" : null}"""); // JsonException: member not found

[JsonMissingMemberHandling(JsonMissingMember.Error)]
public class MyPoco { }

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 41
  • Comments: 29 (15 by maintainers)

Commits related to this issue

Most upvoted comments

All you need to do SetMissingMemberHandling and it will handle every thing for you but you need to install DevBetter.JsonExtensions

var deserializeOptions = new JsonSerializerOptions()
    .SetMissingMemberHandling(MissingMemberHandling.Ignore);

var weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions);

I don’t understand. It’s 2022. And we’re still devising custom solutions in lengthy conversations, in order to be able to deserialize with the absolutely basic option “IF JSON NO MATCH CLASS, DESERIALIZE NO HAPPY” ?

For the last three years I read that Newtonsoft’s Json is dying. and I keep trying to adopt System.Text.Json. And yet I’ll yet again go back to Newtonsoft because I’m baffled by the absolutely wacky solutions proposed to address basic scenarios that were literally one-liners with Newtonsoft.

@LetMeSleepAlready and @SiggyBar The Devbetter Team Package updated so you can find cover for your problem here https://github.com/DevBetterCom/DevBetter.JsonExtensions/blob/main/tests/DevBetter.JsonExtensions.Tests/SetMissingMemberErrorHandlingTests.cs Nuget: DevBetter.JsonExtensions Source Code: DevBetter.JsonExtensions

@ShadyNagy Interesting extension, but I don’t see any code that handles the ‘MissingMemberHandling.Error’ case. So that means it is the default functionality, which is not the intent of this raised ticket.

case:


POCO: class { public string member {get;set;} }
JSON: {"membah","some value"}

The above will not throw an exception, and there is no way to somehow trap this and act on it.

This is a pretty important feature for robust versioning of APIs. Could this be expedited in any way?

Starting with .NET 7, it is possible enable missing member handling for every type without the added boilerplate using a custom resolver:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers = { AddMissingMemberHandling }
    }
};

string json = """{"Name" : "John", "Surname" : "Doe", "Age" : 99 }""";
// Fails with Unhandled exception. System.Text.Json.JsonException: JSON properties Surname, Age could not bind to any members of type MyPoco
JsonSerializer.Deserialize<MyPoco>(json, options);

static void AddMissingMemberHandling(JsonTypeInfo typeInfo)
{
    if (typeInfo.Kind == JsonTypeInfoKind.Object && 
        typeInfo.Properties.All(prop => !prop.IsExtensionData) &&
        typeInfo.OnDeserialized is null)
    {
        var cwt = new ConditionalWeakTable<object, Dictionary<string, object>>(); // Dynamically attach dictionaries to deserialized objects

        JsonPropertyInfo propertyInfo = typeInfo.CreateJsonPropertyInfo(typeof(Dictionary<string, object>), "__extensionDataAttribute");
        propertyInfo.Get = obj => cwt.TryGetValue(obj, out Dictionary<string, object>? value) ? value : null;
        propertyInfo.Set = (obj, value) => cwt.Add(obj, (Dictionary<string, object>)value!);
        propertyInfo.IsExtensionData = true;
        typeInfo.Properties.Add(propertyInfo);
        typeInfo.OnDeserialized = obj =>
        {
            if (cwt.TryGetValue(obj, out Dictionary<string, object>? dict))
            {
                cwt.Remove(obj);
                throw new JsonException($"JSON properties {String.Join(", ", dict.Keys)} could not bind to any members of type {typeInfo.Type}");
            }
        };
    }
}

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

@shadow-cs the feature is not in our immediate sights for .NET 7. Hopefully once https://github.com/dotnet/runtime/issues/63686 has been released it should be possible to convert the workaround as described in https://github.com/dotnet/runtime/issues/37483#issuecomment-950774100 into a general-purpose contract resolver.

Here’s a potential workaround using .NET 6 features:

using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

JsonSerializer.Deserialize<MyPoco>(@"{ ""X"" : 42, ""Y"" : -1 }");
// Unhandled exception. System.Text.Json.JsonException: Contains extra properties: Y.

public class MyPoco : IJsonOnDeserialized
{
    public int X { get; set; }

    [JsonExtensionData]
    public IDictionary<string, object>? ExtraProperties { get; set; }

    void IJsonOnDeserialized.OnDeserialized()
    {
        if (ExtraProperties?.Count > 0)
        {
            throw new JsonException($"Contains extra properties: {string.Join(", ", ExtraProperties.Keys)}.");
        }
    }
}

I just checked the of the json serializer. Maybe it could be as simple as the following (but then again, maybe not)

System.Text.Json.JsonSerializer.LookupProperty

if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty)
            {
                JsonPropertyInfo? dataExtProperty = state.Current.JsonTypeInfo.DataExtensionProperty;
                if (dataExtProperty != null && dataExtProperty.HasGetter && dataExtProperty.HasSetter)
                {
                    state.Current.JsonPropertyNameAsString = JsonHelpers.Utf8GetString(unescapedPropertyName);
                    if (createExtensionProperty)
                    {
                        CreateDataExtensionProperty(obj, dataExtProperty, options);
                    }
                    jsonPropertyInfo = dataExtProperty;
                    useExtensionProperty = true;
                }
// snippet goes here
            }

add the following snippet:

 else if (options.ThrowOnMissingMember) {
 ThrowHelper.ThrowJsonException_DeserializeUnableToBindMember();
}

Of course this would require an new ThrowHelper, and a new property in JsonSerializerOptions (ThrowOnMissingMember, default false = current way of working), both which are minor additions.

I figured the above might at least get this ticket going in some direction.

Any update on this?