Validation is an important part of building reliable .NET applications. Before saving a customer, creating an order, or updating a user profile, an application must check whether the submitted data meets its requirements.
.NET provides DataAnnotations to define validation rules directly on model properties. Attributes such as Required, StringLength, Range, and EmailAddress make common validation tasks straightforward. However, their default error messages may not explain the problem clearly enough for users.
For example, a form might display "The field Email must be a string or array type with a minimum length of 5." That message describes a technical constraint, but it does not tell the user what to enter in simple terms.
Custom validation messages help make errors more useful. They can explain the expected value, identify the relevant field, and guide users toward correcting their input.
This article explains how to customize DataAnnotations validation messages in .NET applications, create reusable validation rules, and avoid common mistakes when handling validation in APIs and web forms.
Understanding DataAnnotations Validation
DataAnnotations are attributes that describe validation rules and other metadata for .NET model properties. The validation system can evaluate these attributes and return validation errors when the supplied values do not meet the rules.
Consider a registration model:
using System.ComponentModel.DataAnnotations;
public class RegisterUserRequest
{
[Required]
public string Name { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Range(18, 100)]
public int Age { get; set; }
}
The model defines three basic requirements:
When validation fails, the framework can return error messages associated with the attributes.
The default messages are useful during development, but application users often benefit from messages written specifically for the form or business process.
Setting Custom Messages With ErrorMessage
The simplest way to customize a validation message is to use the ErrorMessage property provided by many built-in validation attributes.
using System.ComponentModel.DataAnnotations;
public class RegisterUserRequest
{
[Required(ErrorMessage = "Please enter your name.")]
[StringLength(
50,
MinimumLength = 2,
ErrorMessage = "Your name must contain between 2 and 50 characters.")]
public string Name { get; set; } = string.Empty;
[Required(ErrorMessage = "Please enter your email address.")]
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
public string Email { get; set; } = string.Empty;
[Range(
18,
100,
ErrorMessage = "Your age must be between 18 and 100.")]
public int Age { get; set; }
}
Each attribute now provides a message that explains the expected input.
For example, if the user enters an invalid email address, the application can display:
Enter a valid email address.
This message is easier to understand than a framework-generated message.
Choosing the right message
A good validation message should explain the problem and, when appropriate, tell the user how to correct it.
Validation rule | Less helpful message | Better message |
|---|
Required name | The Name field is required. | Please enter your name. |
Minimum password length | The field Password must be a string with a minimum length of 8. | Your password must contain at least 8 characters. |
Invalid email | The Email field is not valid. | Enter a valid email address. |
Age range | The field Age must be between 18 and 100. | Your age must be between 18 and 100. |
Invalid quantity | The field Quantity must be between 1 and 100. | Enter a quantity between 1 and 100. |
The message should match the actual rule. Avoid telling users that a value is invalid without explaining what a valid value looks like.
Using Placeholders in Validation Messages
Some validation attributes support formatted error messages. These allow the message to include the property name or a configured display name.
For example:
using System.ComponentModel.DataAnnotations;
public class ProductRequest
{
[Required(
ErrorMessage = "{0} is required.")]
[StringLength(
100,
MinimumLength = 3,
ErrorMessage = "{0} must contain between {2} and {1} characters.")]
[Display(Name = "Product name")]
public string Name { get; set; } = string.Empty;
}
The placeholders are replaced when the validation message is formatted.
For the StringLength attribute:
{0} represents the display name of the property.
{1} represents the maximum permitted length.
{2} represents the minimum permitted length.
With the display name configured as Product name, the message can be rendered as:
Product name must contain between 3 and 100 characters.
This approach is useful when several properties use similar validation rules.
For example, a customer form may contain a first name, last name, and city. Instead of writing a completely different message for each property, you can use a consistent pattern with display names.
Keep in mind that placeholder support depends on the specific validation attribute. Check the attribute's documentation before assuming that every attribute supports the same placeholders.
Customizing Validation Messages in ASP.NET Core APIs
DataAnnotations are commonly used in ASP.NET Core Web APIs. When a controller accepts a model marked with [ApiController], ASP.NET Core automatically handles model validation and typically returns a 400 Bad Request response when validation fails.
Consider the following controller:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult CreateUser(RegisterUserRequest request)
{
return Ok(new
{
Message = "User created successfully."
});
}
}
If the request contains an invalid email address, the action normally does not execute. The framework returns a validation response containing the errors associated with the model.
Because the validation attributes already contain custom messages, those messages can appear in the response.
For example, a client might receive a response similar to this:
{
"errors": {
"Email": [
"Enter a valid email address."
],
"Age": [
"Your age must be between 18 and 100."
]
}
}
The exact response can vary depending on the framework version, serializer settings, and validation configuration.
Returning a consistent validation response
Applications with multiple APIs may need a consistent error format. For example, a frontend application might expect a general message alongside field-specific errors.
You can customize how invalid model states are returned by configuring InvalidModelStateResponseFactory.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errors = context.ModelState
.Where(entry => entry.Value?.Errors.Count > 0)
.ToDictionary(
entry => entry.Key,
entry => entry.Value!.Errors
.Select(error => string.IsNullOrWhiteSpace(error.ErrorMessage)
? "The supplied value is invalid."
: error.ErrorMessage)
.ToArray());
var response = new
{
Message = "Please correct the validation errors.",
Errors = errors
};
return new BadRequestObjectResult(response);
};
});
var app = builder.Build();
app.MapControllers();
app.Run();
This example returns a predictable structure with a general message and a dictionary of field-specific errors.
The fallback message handles cases where a validation error does not contain a usable message. It should not replace meaningful messages from your validation attributes.
Use this customization only when a consistent response format is useful for your API clients. If your application already relies on ASP.NET Core's standard validation response, changing the format may require updates to existing clients.
Creating a Custom Validation Attribute
Built-in attributes cover many common rules, but business applications often require additional validation.
Suppose a registration form accepts a username that must contain only letters, digits, and underscores. You can implement a reusable attribute by inheriting from ValidationAttribute.
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
public sealed class UsernameAttribute : ValidationAttribute
{
private static readonly Regex UsernamePattern =
new(@"^[A-Za-z0-9_]+$", RegexOptions.Compiled);
public override bool IsValid(object? value)
{
if (value is null)
{
return true;
}
if (value is not string username)
{
return false;
}
return UsernamePattern.IsMatch(username);
}
}
The attribute checks the supplied value against a regular expression.
Returning true for null is intentional. A custom format attribute generally should not duplicate the responsibility of [Required]. If the property is mandatory, apply both attributes.
Use the custom attribute in a model:
public class CreateAccountRequest
{
[Required(ErrorMessage = "Please choose a username.")]
[Username(ErrorMessage =
"Your username can contain only letters, numbers, and underscores.")]
public string Username { get; set; } = string.Empty;
}
The two attributes handle different rules:
Separating these responsibilities makes validation easier to maintain and reuse.
For a real application, also consider username length, reserved names, case sensitivity, and database uniqueness. A regular expression alone does not guarantee that a username can be registered successfully.
When custom attributes are appropriate
A custom validation attribute is useful when a rule is:
Reused across several models.
Independent of external services or database state.
Easy to express as a deterministic check.
Expected to produce a consistent validation message.
Rules that require database queries, remote service calls, or access to other application services are often better implemented in an application service or dedicated validation layer.
For example, checking whether an email address already exists in the database is not the same as checking whether its format is valid. The format can be validated locally, while uniqueness should be enforced through appropriate application and database logic.
Using ValidationAttribute for More Detailed Messages
Sometimes a custom attribute needs to generate a message based on the value or configuration.
You can override FormatErrorMessage to control how the message is formatted.
using System.ComponentModel.DataAnnotations;
public sealed class MinimumOrderQuantityAttribute
: ValidationAttribute
{
public int MinimumQuantity { get; }
public MinimumOrderQuantityAttribute(int minimumQuantity)
{
if (minimumQuantity < 1)
{
throw new ArgumentOutOfRangeException(
nameof(minimumQuantity));
}
MinimumQuantity = minimumQuantity;
}
public override bool IsValid(object? value)
{
if (value is null)
{
return true;
}
return value is int quantity &&
quantity >= MinimumQuantity;
}
public override string FormatErrorMessage(string name)
{
return $"{name} must be at least {MinimumQuantity}.";
}
}
Apply the attribute to a request model:
public class OrderRequest
{
[MinimumOrderQuantity(5)]
public int Quantity { get; set; }
}
When validation fails, the message can include the configured minimum quantity.
This pattern is useful when the same rule applies to different properties but the required threshold varies.
For example, a product might require a minimum order of five units, while another product requires a minimum order of ten. The attribute can express the numeric requirement, although rules that depend on the selected product may be more appropriately handled by application-level validation.
Localizing Validation Messages
Applications used in multiple countries may need validation messages in different languages.
Hardcoding English messages in every attribute makes localization more difficult. DataAnnotations support resource-based error messages through ErrorMessageResourceType and ErrorMessageResourceName.
For example, suppose a resource class named ValidationMessages contains a resource entry named NameRequired.
using System.ComponentModel.DataAnnotations;
public class CustomerRequest
{
[Required(
ErrorMessageResourceType = typeof(ValidationMessages),
ErrorMessageResourceName = nameof(ValidationMessages.NameRequired))]
public string Name { get; set; } = string.Empty;
}
The resource class represents generated resource accessors for a .resx file. In a real project, create the resource file and add the corresponding entry before using this example.
You can provide translated resource files for supported cultures. ASP.NET Core localization middleware and request-culture configuration can then determine which culture applies to the request.
This keeps validation messages separate from business logic and makes it easier to maintain translations.
When localizing messages, pay attention to grammar and placeholder ordering. A sentence structure that works in English may not work in another language if the property name and numeric values appear in different positions.
Common Mistakes to Avoid
Assuming Required rejects every invalid value
[Required] checks whether a value is present. For strings, it also rejects empty or whitespace-only values by default. It does not validate email formats, numeric ranges, or business-specific rules.
Combine attributes when a property has multiple requirements.
Duplicating validation rules
If the same rule appears in several models, the messages and permitted values can become inconsistent.
Use a shared custom attribute or a centralized validation component when the rule is genuinely reusable. Avoid building abstractions for rules that appear only once and are unlikely to be reused.
Treating client-side validation as sufficient
Browser validation improves usability, but clients can bypass it. Validate incoming requests on the server before processing them.
For rules that protect data integrity, enforce the relevant constraints in the application and, where appropriate, the database as well.
Performing database queries inside validation attributes
A validation attribute should generally remain predictable and lightweight. Database access introduces dependencies, asynchronous execution concerns, and additional complexity.
Use an application service or a dedicated validator for rules that require external data.
Changing API validation responses without checking clients
Frontend applications may depend on the default ASP.NET Core validation response. Changing its structure can break existing integrations.
Document the response contract and update API tests whenever the format changes.
Best Practices for Production Applications
Use these practices to keep validation behavior consistent and maintainable:
Write messages that explain the expected input rather than exposing framework terminology.
Keep format validation separate from required-field validation when the rules have different responsibilities.
Use display names and supported placeholders to reduce repeated message text.
Create custom attributes for reusable, local validation rules.
Keep database-dependent and cross-service validation in an appropriate application layer.
Validate requests on the server even when the user interface performs client-side validation.
Standardize API error responses when multiple clients consume the same API.
Add tests for valid input, invalid input, boundary values, and missing values.
Store translatable messages in resources when localization is required.
Review error messages to ensure they do not expose internal implementation details or sensitive information.
Advantages and disadvantages
Approach | Advantages | Disadvantages |
|---|
ErrorMessage property
| Simple and easy to maintain | Repeated text can become difficult to manage |
Placeholders and display names | Supports consistent messages across fields | Placeholder support varies by attribute |
Custom validation attribute | Reusable for local business rules | Can become difficult to maintain if it handles too many responsibilities |
Resource-based messages | Supports localization and centralized wording | Requires resource files and localization setup |
Custom API validation response | Gives clients a consistent response structure | Can break clients that expect the default format |
Choose the simplest approach that meets the application's requirements. Built-in attributes with custom messages are enough for many forms. More advanced techniques should solve a real maintenance or integration problem.
Summary
DataAnnotations provide a straightforward way to validate .NET models, but the default error messages are not always suitable for application users.
Setting ErrorMessage values makes common validation rules easier to understand. Display names and placeholders help maintain consistency, while custom attributes support reusable rules that are not covered by built-in validation attributes.
For ASP.NET Core APIs, the validation response can also be customized to provide a consistent structure for client applications. Resource-based messages are useful when an application supports multiple languages.
The key is to keep validation understandable and focused. Use DataAnnotations for local model rules, place database-dependent checks in the appropriate application layer, and always validate untrusted input on the server.
Join the conversation! Your thoughts help the community grow.