← Main

Disabling the required modifier informing System.Text.Json

by David Sherret

Today, I looked into C# 11's new features, which include the required modifier. According to the docs, this is what it does:

The required modifier indicates that the field or property it's applied to must be initialized by an object initializer. Any expression that initializes a new instance of the type must initialize all required members. The required modifier is available beginning with C# 11. The required modifier enables developers to create types where properties or fields must be properly initialized, yet still allow initialization using object initializers.

This is great because I can now define a type like the following:

public record MyRecord
{
  public required string MyValue { get; set; }
}

And when I go to initialize it I will get a compile time error when not specifying this property in an object initializer, similar to how TypeScript works by default:

var myValue = new MyRecord
{
  // Error - CS9035 - Required member 'MyRecord.MyValue' must be
  // set in the object initializer or attribute constructor.
};

required modifier and System.Text.Json

Here lies the problem. After upgrading some code to use required, I started getting runtime exceptions. The reason is that in .NET 7, three ways were added to mark a property or field as required for JSON deserialization:

There are three ways to mark a property or field as required for JSON deserialization:

Source

In my opinion, and without knowing all the details, the required modifier should not have been on that list. It all boils down to this:

Ensuring that a property appears in an object initializer and ensuring that a property is required for JSON serialization are separate matters.

Exception 1 - Nullable types without a JSON property

Take this example:

using System.Text.Json;

var result = JsonSerializer.Deserialize<MyRecord>("{}");
Console.WriteLine(result?.MyValue);

public record MyRecord
{
  public string? MyValue { get; set; }
}

Here I have a nullable string property. In this example, it will deserialize to null because it doesn't appear as a property in the empty JSON object. This code works fine.

Now let's upgrade to C# 11 and take advantage of the required keyword to ensure that this property is always assigned to in an object initializer:

public record MyRecord
{
  public required string? MyValue { get; set; }
}

We've just created a runtime exception in the above code.

System.Text.Json.JsonException: 'JSON deserialization for type 'MyRecord' was missing required properties, including the following: MyValue'

In my opinion, the required modifier should have no effect on this and deserialization should not throw an exception similar to before. This would be a far less error-prone default for users of the API. How often do developers care about nullable properties not appearing in the JSON? Nullable properties are often excluded to reduce the serialized data's size.

Instead, if I wanted this behaviour, I should instead have been able to opt into it via the JsonRequiredAttribute:

// my desired API for the above behaviour
public record MyRecord
{
  [JsonRequired]
  public required string? MyValue { get; set; }
}

.NET Runtime issue that was closed as by design: #76527

Exception 2 - Ignoring a property with a required modifier in deserialization

Say we have some data that we use in our application on the server and we also want to send it to the client, but without a property. This type is only ever serialized and never deserialized.

Instead of defining a new type, we could be a bit lazy and just mark the property as ignored via [JsonIgnore].

public record MyRecord
{
  // should be sent to the client
  public string MyClientProperty { get; set; } = null!;
  // should only be accessible on the server and not sent to the client
  [JsonIgnore]
  public string MyServerProperty { get; set; } = null!;
}

This works fine. Let's upgrade to using the required modifier in C# 11 to ensure the server always assigns to this property in object initializers:

public record MyRecord
{
  // should be sent to the client
  public required string MyClientProperty { get; set; }
  // should only be accessible on the server
  [JsonIgnore]
  public required string MyServerProperty { get; set; }
}

We've unfortunately just introduced a runtime exception in our code:

System.InvalidOperationException: 'JsonPropertyInfo 'MyServerProperty' defined in type 'MyRecord' is marked required but does not specify a setter.'

This fails because System.Text.Json does serialization AND deserialization validation on the type. The required modifier means this will fail deserialization (which we won't ever do in this case). If anything, it seems there should be a way to mark a type as serializable only, similar to what serde does.

.NET Runtime issues that were closed as by design:

Solution

The above two runtime exceptions were just what I ran into within a few minutes of trying out the required modifier so there might be more.

To figure out how to get my desired behaviour, within System.Text.Json we can see it does the following to determine if a property is required or not (Source):

propertyInfo.IsRequired =
  memberInfo.GetCustomAttribute<JsonRequiredAttribute>(inherit: false) != null
    // shouldCheckForRequiredKeyword is based on the context of where the property appears
    || (shouldCheckForRequiredKeyword && memberInfo.HasRequiredMemberAttribute());

Luckily, they have provided a way to override this and a hint is given on the Required properties page I linked to earlier. Essentially, we need to create a custom TypeInfoResolver that builds upon the functionality of the DefaultJsonTypeInfoResolver to only set a property as required when it has the JsonRequiredAttribute.

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

var result = JsonSerializer.Deserialize<MyRecord>(
  "{}",
  new JsonSerializerOptions
  {
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
      Modifiers =
      {
        static typeInfo =>
        {
          foreach (var info in typeInfo.Properties)
          {
            if (info.IsRequired)
            {
              info.IsRequired = info.AttributeProvider?.IsDefined(
                typeof(JsonRequiredAttribute),
                inherit: false
              ) ?? false;
            }
          }
        }
      }
    },
  }
);

Console.WriteLine(result?.MyValue);

public record MyRecord
{
    // [JsonRequired] // uncomment the attribute to see this take effect
    public required string? MyValue { get; set; }
}

In ASP.NET Core, you can set this globally when configuring your JSON options:

builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.TypeInfoResolver =
      new DefaultJsonTypeInfoResolver
      {
          // same code as above goes here
      };
});

Now, you can upgrade to liberally using the required modifier without worrying about introducing probably needless System.Text.Json runtime exceptions.

Hope it helps!