C# 11 Required Members: Stop Shipping Half-Initialized Objects
C# 11's required modifier turns 'trust me, someone sets this property' into a compile-time guarantee. Here's how it works, where SetsRequiredMembers fits, and why deserializers can still bypass it.
22 Mar 2026, 05:30 UTC

If you've ever written string Name { get; init; } = null! just to silence the nullable-reference-type compiler, you've felt the problem C# 11's required modifier solves. The null! is a lie you tell the compiler: "trust me, someone will set this." Nothing enforces that promise. Required members turn the lie into a compile-time guarantee — and this post shows when that guarantee is worth taking, and when a plain constructor is still the better tool.
The gap required members close
Before C# 11, a type with mutable or init-only properties had no way to say "you must set this." Constructors could enforce it, but constructor parameter lists get unwieldy for models with many values, and they fight against the object-initializer style that serializers, test builders, and options-binding code prefer. So teams fell back on conventions, code review, and the null! suppression.
The required modifier (available with C# 11, which ships with the .NET 7 SDK and later) changes the contract: the compiler rejects any object creation it can see unless every required member is assigned in an object initializer. Omit one and you get error CS9035 naming the missing member — at build time, not in production.
A worked example: a queue payload
Imagine a message payload where a missing topic or timestamp is a real bug, not a cosmetic one:
public class QueueMessage
{
public required string Topic { get; init; }
public required DateTimeOffset EnqueuedAt { get; init; }
public string? CorrelationId { get; init; } // optional
}
// Compiles: all required members assigned
var ok = new QueueMessage
{
Topic = "orders.created",
EnqueuedAt = DateTimeOffset.UtcNow
};
// Fails with CS9035: 'EnqueuedAt' must be set
var bad = new QueueMessage { Topic = "orders.created" };Compare that to the pre-C# 11 version: public string Topic { get; init; } = null!; compiles fine with nothing assigned, and the failure surfaces later as a NullReferenceException somewhere downstream. The required keyword expresses the intent directly instead of encoding it as a suppression.
The escape hatch: SetsRequiredMembers
Sometimes a constructor genuinely does assign everything — a factory method, for instance. Annotating it with [SetsRequiredMembers] (from System.Diagnostics.CodeAnalysis) tells the compiler to trust it:
using System.Diagnostics.CodeAnalysis;
public class QueueMessage
{
public required string Topic { get; init; }
public required DateTimeOffset EnqueuedAt { get; init; }
[SetsRequiredMembers]
public QueueMessage(string topic)
{
Topic = topic;
EnqueuedAt = DateTimeOffset.UtcNow;
}
}
var msg = new QueueMessage("orders.created"); // no initializer neededTwo cautions here. First, the attribute is a promise the compiler does not fully verify — if the constructor forgets a member, callers get no warning. Second, inheritance composes: a derived class inherits required members, and its constructors must either chain to a SetsRequiredMembers base constructor or leave callers to satisfy everything via initializers.
The limitation that matters most: deserialization
Enforcement is compile-time only, and only for construction the compiler can see. Deserializers create objects through reflection, bypassing your initializers entirely. System.Text.Json in recent .NET versions does honor C# required members during deserialization, but this is version-sensitive and is a different mechanism from [JsonRequired] — don't conflate them. If your type crosses a serialization boundary, round-trip it in your target framework and observe whether a missing JSON property throws or silently defaults. Similarly, test builders and mapper libraries that construct via reflection won't trigger CS9035.
There's also a design trade-off: for types with many mandatory values, a positional record (record QueueMessage(string Topic, DateTimeOffset EnqueuedAt);) is often the more idiomatic answer — immutability plus enforced construction in one line. Required members shine for mutable models, initializer-heavy configuration objects, and hierarchies where constructors would multiply.
Verify it yourself
Before adopting the feature, confirm your toolchain. Check that your project targets net7.0 or later (or sets <LangVersion>11.0</LangVersion> or higher in the csproj) — older pinned language versions won't compile the keyword. Then, in a scratch console project:
- Declare a class with a
required stringproperty and writenew Foo()with no initializer. Build and confirm CS9035 names the missing member. - Add a
[SetsRequiredMembers]constructor that assigns the member and confirmnew Foo(...)compiles cleanly. - Deserialize JSON with a missing property via
JsonSerializer.Deserialize<Foo>in your actual target framework and note the behavior — exception or silent default.
The actionable takeaway: reach for required when you want initializer syntax with constructor-level safety, keep positional records for small immutable shapes, and never assume the guarantee survives a serializer without testing it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.