Ранее я написал об этом пост, но потом набралось больше материала и я расширил до статьи.
Работая над одним из проектов, который недавно переехал из Framework 4.8 на Core 9, обнаружил множество самых разных вариантов использования модификатора required и атрибута Required, примерно каждый второй из которых был использован неправильно. Я написал это коллегам и хочу поделиться этим здесь. Это не обязательные правила, но сильно упрощают работу с кодом.

Атрибут Required и модификатор required
Атрибут Required нужен для проверки входящих преимущественно строковых данных в эндпоинтах. Возвращает ошибку, если значение null или пустая строка для строк (если не отключено параметром AllowEmptyStrings). Работает в Runtime. Также применяется в Entity Framework в подходе code-first но с включением опции <Nullable> в csproj про эти случаи можно забыть, сделав код чище.
Модификатор required нужен для обязательного указания значений полей при создании класса. Работает в Compile-time.
Примеры использования
// имеем класс с required полем
public class Example
{
public required string Name { get; set; }
}
// пытаемся создать экземпляр в коде
var example1 = new Example(); // будет ошибка при попытке сборки проекта
var example2 = new Example { Name = string.Empty }; // тут ошибки не будет
// Вывод: модификатор required нужен для разработчика
// имеем класс с полем, у которого атрибут Required
public class Example
{
[Required]
public string Name { get; set; }
}
// пытаемся создать экземпляр в коде
var example = new Example(); // проект спокойно собирается
// имеем эндпоинт в контроллере
public IActionResult PostMethod([FromBody] Example model) => Ok();
/* передаём в теле запроса:
{}
или
{"Name": null}
или
{"Name": ""}
или
{"Name": " "}
Получаем BadRequest с текстом ошибки. */
// передаём в теле запроса: {"Name": "name"}. Получаем OK.
// Вывод: атрибут Required нужен для пользователя
Как стоит и не стоит использовать
public class BadExample
{
public required string Field1 { get; set; } // 1
public required string? Field2 { get; set; } // 2
[Required]
public required string Field3 { get; set; } // 3
[Required]
public string? Field4 { get; set; } // 4
[Required]
public int Field5 { get; set; } // 5
public required int Field6 { get; set; } = 10; // 6
public required List<int> Field7 { get; set; } // 7
}
Ошибка, если класс используется как входящий параметр в эндпоинте. Соответственно, не стоит использовать, если десериализуем в него. Это создаёт избыточную сложность.
Либо
required, либоnullable.Надо выбрать одно из двух в зависимости от места использования.
Либо
Required, либоnullable.Requiredиспользуем для строк. Но есть нюанс (*).Не нужно использовать
requiredсо значением по умолчанию.Не стоит усложнять жизнь, если поле можно проинициализировать при создании класса.
public class GoodExample
{
public required string Field1 { get; set; } // 1
[Required]
public string Field2 { get; set; } = null!; // 2
public string? Field3 { get; set; } // 3
public int Field4 { get; set; } // 4
public List<string> Field5 { get; set; } = []; // 5
}
Хорошо где угодно за пределами эндпоинтов и десериализации, а значение не может принимать
null.То что нужно для эндпоинта.
Поле
nullable. Поэтому никакихrequired.Не используем атрибут
Requiredс не строками. Но есть нюанс (*).Избегаем использование
required, проинициализировав коллекцию.
* - если передаётся json, в котором явно указано значение null ({"IntField": null}), то использование атрибута Required вернёт BadRequest с текстом ошибки валидации.
Если же в json поле было опущено, то будет присвоено значение по умолчанию.
Самописные атрибуты
Вот код, который скрывается за базовым атрибутом Required:
public override bool IsValid(object? value)
{
if (value is null)
return false;
return AllowEmptyStrings || value is not string stringValue || !string.IsNullOrWhiteSpace(stringValue);
}
Допустим, я хочу написать свой атрибут, который будет проверять ещё и не nullable коллекции, чтобы там был хотя бы один элемент. Тут всё просто: создаём новый класс, который наследуется от RequiredAttribute и переписывается метод IsValid.
А если мне надо, чтобы атрибут возвращал ошибку не на одном языке, а в зависимости от настроек пользователя? Или мне ещё десяток уникальных атрибутов надо с подобными возможностями? Тут аналогично: наследуем новый класс уже от ValidationAttribute и переписываем чуть больше. А лучше даже создаём класс с общей логикой для всех будущих атрибутов, в которых вся внутренняя логика будет укладываться всего в несколько строк. Красота!
И там CustomRequiredAttribute, и тут CustomRequiredAttribute, "но есть нюанс", Петька. Валидация полей работает как по маслу, пока не решишь применить этот атрибут к параметрам эндпоинтов. Если мы воспользовались вторым вариантом, то внезапно выясним, что на каждый помеченный новым атрибутом параметр прилетает две ошибки, если он null. Перепроверяем, посылая пустую коллекцию - получаем одну ошибку. Перепроверяем с null - снова две.
Начинаем копать и находим такую опцию с очень длинным названием, которое говорит само за себя:
builder.Services.AddControllers(options =>
{
options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
});
Само собой. мы её включаем и пробуем ещё раз, посылая null, ожидая, что всё заработает. И всё работает, но полностью игнорируя и неявный базовый RequiredAttribute, и явный самописный, унаследованный от ValidationAttribute. Какие бы мы манипуляции не делали, с null в значении параметра эндпоинта и опцией контроллера, мы получаем либо две одинаковые ошибки, либо ни одной.
// в .csproj: <Nullable>enable</Nullable>
[HttpPost]
public IActionResult SomeMethod([FromBody, CustomRequired] List<int> ids) => Ok();
Решение оказалось и очевидным, и нет. Чтобы заработало как надо, именно в этом случае самописный атрибут Required нужно наследовать от RequiredAttribute. Тогда ваш самописный атрибут подменяет собой базовую логику. Казалось бы, какая разница? Только вот RequiredAttribute у фреймворка на особом счету, что даже удостоился отдельной опции.
НО! С включением опции SuppressImplicitRequiredAttributeForNonNullableReferenceTypes все места, где RequiredAttribute срабатывал неявно, нужно теперь его или его наследника всегда явно указывать.
// тут всё без изменений
[HttpPost]
public IActionResult SomeMethod([FromBody] string? str) => Ok();
// с выключенной опцией раньше неявно срабатывает RequiredAttribute, если приходит null
[HttpPost]
public IActionResult SomeMethod([FromBody] string str) => Ok();
// со включенной опцией теперь нужно явно указывать, иначе пропустим null
[HttpPost]
public IActionResult SomeMethod([FromBody, Required] string str) => Ok();
Получается такая ситуация с самописными атрибутами:
// подходящий вариант для всего, кроме параметров
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class CustomRequiredAttribute
: BaseCustomValidationAttribute // базовый для всех самописных со всей общей логикой
{
public bool AllowEmptyStrings { get; set; }
public bool AllowEmptyCollections { get; set; }
public CustomRequiredAttribute()
: base("Default error template")
{
}
public override bool IsValidLogic(object value, ValidationContext validationContext) =>
value switch
{
null => false,
ICollection c => AllowEmptyCollections || c.Count > 0,
string s => AllowEmptyStrings || !string.IsNullOrWhiteSpace(s),
_ => true
};
}
// подходящий вариант для всего, особенно для параметров
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class CustomRequiredAttribute
: RequiredAttribute // стандартный. Поэтому логику придётся писать отдельно
{
public bool AllowEmptyStrings { get; set; }
public bool AllowEmptyCollections { get; set; }
public CustomRequiredAttribute()
: base("Default error template")
{
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var result = value switch
{
null => false,
ICollection c => AllowEmptyCollections || c.Count > 0,
string s => AllowEmptyStrings || !string.IsNullOrWhiteSpace(s),
_ => true
};
if (result)
return ValidationResult.Success;
// некоторая логика, которая в ином случае была бы в BaseCustomValidationAttribute
return new ValidationResult(...);
}
}
Со всеми остальными атрибутами подобных проблем не наблюдалось. Можно спокойно писать свои, для параметров и полей, наследуя их от ValidationAttribute.
Будьте внимательны в использовании слова required где бы то ни было в проекте и всегда пишите тесты. Как юнит-тесты, так и интеграционные, чтобы отлавливать подобные проблемы заранее. Именно тесты меня подтолкнули к написанию статьи.
Надеюсь, она поможет сделать код чище и избежать неоднозначностей.
Комментарии (15)

AgentFire
09.01.2026 14:02Либо required, либо nullable.
Громко не соглашусь. Вы вот в своей статье почему-то даже не написали для чего, собственно, нужно это ключевое слово. Только как оно работает. Вы также не обосновали это ваше утверждение, хотя оно мелькнуло минимум дважды за статью.
А я вот поделюсь своим личным мнением, для чего оно нужно. На мой взгляд, его единственная цель - это не дать забыть про инициализацию этого поля разработчиком, который пишет new().
В этом случае как тип, так и nullability поля не имеет значения. На разработчика накладывается обязательство explicitly (-over implicitly) указать изначальное значение, даже если оно null/default.
Таким образом чтение кода сильно упрощается - в будущем любому читающему не будет непонятно, писатель кода забыл указать значение, или же оно там действительно должно быть проинициализировано значением "null".

ProgerMan Автор
09.01.2026 14:02Я не вижу ни одного реального примера, для которого может понадобиться обязательно указывать
nullдляnullable. Допустим, сделали неявное явным, но как это поможет? На мой взгляд это лишь загромождает код. Если в дальнейшем коде устраивает значение по умолчанию, то модификаторrequiredне нужен.
AgentFire
09.01.2026 14:02Я же русским по темному написал обоснование необходимости и как именно это поможет, аж в двух местах.

WhiteBehemoth
09.01.2026 14:02как пример - вы допускаете установку поля в null, но хотите избежать случайного обнуления (когда поле просто случайно опущено).

voroninp
09.01.2026 14:02+1
Nullability — это про область определения/множество допустимых значений. То, что null в этом случае соответствует значению по умолчанию не более чем нюанс (иногда, кстати, очень досадный).
required — про явную инициализацию
Понятия ортогональные.
WhiteBehemoth
Это не нюанс, это нюансище. Чтоб опущенный параметр для всяких Int или Guid выкидывал ошибку валидации, а не инициализировался "по умолчанию".
Мы в конце концов практически отказались от него. Хлопот больше, чем реальной пользы.
ProgerMan Автор
Для других типов я предпочитаю использовать более конкретные атрибуты, вроде
Rangeили прочие самописные (например,IsPositive). А для более сложных валидаций пишу валидаторы, которые заполняютModelStateошибками.AgentFire
А мы наоборот, используем в своих DTO его по максимуму. Польза: мы всегда уверены, что все нужные поля будут проинициализировано любым пользователем DTO. Хлопот никаких нет. А какие они у вас?
ProgerMan Автор
В своих проектах всё, что можно провалидировать атрибутами, валидирую атрибутами. От этого особенно выигрывают проекты на Blazor, т.к. там это работает, как на сервере, так и на клиенте.
AgentFire
Простите, а причем тут валидация? Речь про
required, не[Required].WhiteBehemoth
required в DTO даже не думали. У нас вся валидация через атрибуты. (DTO используется только API на базе ASP.NET). Интересно, если required поле не было в приходящем json объекте, ASP.NET возвращает такой же 400 bad request с пропущенным полем, как и с [Required] атрибутом?
Хлопоты, про которые я говорил - накладываемые ограничения, типа конструктора без параметров. Мы хотели убрать
null!инициализацию для not nullable properties, но оказались не готовы для этого менять логику.AgentFire
Все еще непонятно, причем тут какая-то валидация. Ключевое слово
requiredвообще с ней никак не связано.Но ведь с
requiredконструктор всё еще запросто может быть без параметров. Что я не так понял в вашем сообщении?Снова непонятно. Вы осознанно поощряете отсутствие инициализации некоторых полей, но введение
required, которое вам бы прогарантировало исправление данной ситуации, вы называете "не готовы менять логику" — т.е. по факту не готовы вручную проинициализировать все пропущенные поля, и сиё утверждение считаете достаточным аргументом противrequired? Всё так понял, или я снова затупил?WhiteBehemoth
Про валидацию, - потому, что речь о DTO. Проверка атрибута
[required]- часть процесса валидации. Смысл там такой же, как и у ключевого словаrequired- убедиться, что клиент предоставит значение для обозначенного поля. Но атрибут более стандартный, поэтому для нашего ASP.NET API мы использовали его.вы так эмоционально не понимаете, будто я вас убеждаю отказаться от использования
requiredв вашем коде.Когда это ключевое слово ввели, у нас было уже написано очень много кода и все классы, где поля обязаны инициализироваться клиентом имели соответствующие конструкторы. Была мысль использовать его шире, убрав инициализацию в
nullnot nullable fields and properties, но это требовало менять инициализацию в куче мест. И да, для нас это было достаточным аргументом не внедрять эту фичу языка в существующий проект.AgentFire
Простите, но мне стало еще непонятнее.
Во-первых, валидация с атрибутом
[Required]абсолютно никак не связаны с ключевым словомrequired. У них разные предназначения."убедиться, что клиент предоставит значение для обозначенного поля" - это ближе к атрибуту. А ключевое слово нужно исключительно при создании экземпляров класса, причем именно через
new. (Поддержка разными десериализаторами этого ключевого слова и соответствующее [Required]-like поведение является исключительно следствием появления самогоrequired, но никак не наоборот).У меня скорее складывается подозрение, что у вас ошибочное представление о том, что такое
requiredи зачем он нужен.Вот например здесь. Если вы действительно хотели ввести ключевое слово
required, то, во-первых, вам не понадобилось бы никакихnull, а, во-вторых, его легко ввести во все нужные поля даже при наличии существующих конструкторов. И даже на этом этапе от него уже пойдёт определённая польза!Поэтому либо у вас сильно искажено понимание этого ключевого слова, либо мы вообще говорим о разных языках программирования.
WhiteBehemoth
В новых проектах, возможно, контракты на обязательность (кроме DTO) будут через
required. А может быть так и останемся во тьме конструкторов с параметрами и прочими фабриками вместо продвинутого ключевого слова. "There are different ways to skin a cat", как говорит мой коллега.