DEV Community

nikosst
nikosst

Posted on

Why I Prefer string.Empty Over null! in Input DTOs

Small coding choices can make software easier to understand and maintain. One example in C# is how we initialize string properties in DTOs.

A DTO Data Transfer Object carries data between parts of an application. For example, it might carry a customer’s name from an API request into the application.

Consider these two approaches:

public sealed class CreateCustomerDtoWithNull
{
    public string Name { get; init; } = null!;
}
public sealed class CreateCustomerDto
{
    public string Name { get; init; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

Both avoid the compiler warning about an uninitialized non-nullable property. Both initial values can be rejected by FluentValidation.

But they create different starting states.

For required text fields where missing and empty values are treated the same, I prefer string.Empty: it gives the property an actual string value without suppressing a compiler warning.

The difference becomes clearer when we look at what each declaration means.

With nullable reference types enabled, string expresses the intention that a property should not contain null.

However:

public string Name { get; init; } = null!;
initializes that property with null.
Enter fullscreen mode Exit fullscreen mode

The ! suppresses the nullable warning. It does not create a value, check anything at runtime, or guarantee that something will replace the null later. Microsoft explicitly documents that the operator has no runtime effect.

Microsoft: null-forgiving operator.

With:

public string Name { get; init; } = string.Empty;
Enter fullscreen mode Exit fullscreen mode

the property starts with "": an actual string containing zero characters. It is different from " ", which contains a space.

Microsoft: String.Empty.

This makes the initial state more consistent with the declared type. We have a string; we still need to check whether its content is acceptable.

“FluentValidation can catch both” is correct.

For example:

using FluentValidation;
public sealed class CreateCustomerDtoValidator
    : AbstractValidator<CreateCustomerDto>
{
    public CreateCustomerDtoValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .WithMessage("Name is required.");
    }
}
Enter fullscreen mode Exit fullscreen mode

For strings, NotEmpty() rejects all of these:
Value Result
null Rejected
"" Rejected
" " Rejected
"Maria" Accepted

This behavior is documented by FluentValidation and can also be checked in its implementation. FluentValidation documentation, NotEmptyValidator source code.

The preference for string.Empty therefore concerns the object’s starting state and the clarity of the code.

It separates two responsibilities:

  • Initialization: give the property a non-null starting value.
  • Validation: decide whether the supplied content meets the requirements. A blank name is still invalid for creating a customer. Having a string value and having a valid customer name are different requirements.

Also, validation does not fill in the missing name. It reports the problem so the application can reject the request.

In an ASP.NET Core application using Clean Architecture, a straightforward execution flow is:

API receives the request
→ DTO is created
→ Application validates the input
→ Domain creates a valid customer
→ Infrastructure saves the customer

The Application layer can execute validation before continuing:

await validator.ValidateAndThrowAsync(dto, cancellationToken);

// Continue with customer creation only after validation succeeds.
The API must then translate validation failures into an appropriate response, such as HTTP 400 with useful error messages.

The Domain should also protect its own essential rules when creating a customer. This keeps those rules effective when data arrives through another route.

Clean Architecture supports this separation of responsibilities. It does not prescribe a particular DTO initializer.

Microsoft: common application architectures.

For a manager, the practical benefit is straightforward: clear starting states reduce the assumptions developers need to make. Reviewers can see what the object initially contains and where unacceptable input is rejected.

There are limits to this preference.

First, string.Empty only sets the initial value. Depending on serializer settings, an incoming JSON value such as "name": null can replace it. External input still needs validation.

Microsoft: nullable annotations in System.Text.Json.

Second, sometimes null carries useful information. If a DTO should accurately represent missing input, a nullable property can be more appropriate:

public string? Name { get; init; }

The business rule can still require a name. The nullable type acknowledges that an incoming request may fail to provide one.

There are other reasonable designs too. Maarten Balliauw discourages using default! to silence warnings in JSON DTOs and discusses alternatives such as nullable properties, constructors, and required. His article supports avoiding hidden assumptions, rather than a universal preference for empty strings. Maarten Balliauw: nullable warnings in JSON models.

Ian Griffiths provides a useful counterargument: an empty string can also hide missing meaningful data if it is added simply to silence the compiler. This matters especially when objects are expected to be valid immediately after construction. Ian Griffiths: defeating the point with empty strings.


Remember..!

For input DTOs where missing and empty text have the same meaning, use string.Empty and validate required content with NotEmpty(). Use nullable properties when absence matters. Use null! when there is a specific, justified initialization guarantee.

The aim is to keep the declared intention, actual starting value, and validation rules consistent and easy for the next person to understand.

How do you handle this in your projects? Do you prefer string.Empty, null!, or another approach for input DTOs? I’d welcome different perspectives, along with your reasoning and examples from experience. In which situations do you think another choice is a better fit?


[email protected]

Top comments (0)