One of the fundamental principles of a good REST API is input data validation and clear communication of validation errors to the consuming service. In ASP, we have dedicated attributes and the ProblemDetails
class that complies with RFC 7807.
Almost always in an API, we return errors that we can signal through HTTP status codes. However, often this information is only a basic error indicator, not allowing us to determine the exact cause. Therefore, in March 2016, a standard was defined to unify error responses in API interfaces.
A definition compliant with RFC 7807 should look like this, for example:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-52f8eaa452b5b1ab506c5abb29e7e6e7-5aa2a02d9a445576-00",
"errors": {
"Title": [
"The Title field is required.",
"The field Title must be a string with a minimum length of 5 and a maximum length of 15."
]
}
}
The most interesting section for us is the Errors array. The Title property should be a short message that is human-readable according to the specification. The standard also allows for error title localization.
Of course, just returning the appropriate JSON doesn’t solve the problem; we still need to remember to return the correct HTTP response code. In the example above, 400 - BadRequest is represented by the Status property.
Developers from Redmond, starting with .NET Core 2.1, by default return a class implementing the above standard for API projects when using validation attributes. Therefore, for most cases, it is sufficient to use dedicated validation attributes in the model class accepted in the API method body.
public class TodoItem
{
[Required]
[StringLength(15, MinimumLength = 5)]
public string Title { get; set; }
[Required]
public DateTime ValidTo { get; set; }
[MinLength(5)]
public string SubTitle { get; set; }
[EmailAddress]
public string NotifyEmail { get; set; }
}
//...
public class TodoController : ControllerBase
{
//...
[HttpPost]
public void Post([FromBody] TodoItem value)
{ }
//...
}
After calling the above method with incorrect input data, we will receive an error:
{
"title": "Test",
"validTo": "2022-06-07T18:45:49.742Z",
"subTitle": "string",
"notifyEmail": "userexample"
}
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-a70409b3919da21ece51e7c524db7684-80db0ffd6d864f8e-00",
"errors": {
"NotifyEmail": [
"The NotifyEmail field is not a valid e-mail address."
]
}
}
Examples of attributes available in the System.ComponentModel.DataAnnotations namespace System.ComponentModel.DataAnnotations
:
[Required]
- Required field[EmailAddress]
- Email field[StringLength]
- Character length constraint[RegularExpression]
- Regular expressionNot always built-in attributes cover our cases, so the possibility of implementing custom attributes comes in handy. For validation attributes, we need to implement the ValidationAttribute abstract class.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class ContainsA: ValidationAttribute
{
internal static string ErrorMessageMockup => "String don't contain A letter";
public ContainsA() : base(() => ErrorMessageMockup) { }
public override bool IsValid(object? value)
{
if (!(value is string) || value == null)
return false;
return ((string)value).Contains("A");
}
}
This attribute checks if strings contain at least one letter “A.”
We are not always able to validate input data with attributes, such as when checking with an external service or database. In such cases, we can return an instance of the ProblemDetails
object directly in the controller. However, in this case, remember that the responsibility for returning the correct HTTP error code and completing the OPEN API documentation lies with us.
// POST: api/Todo
[HttpPost]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ValidationProblemDetails))]
public ActionResult Post([FromBody] TodoItem value)
{
if (!value.Title.Contains("A")) {
ModelState.AddModelError("Title", "String don't contain A letter");
return ValidationProblem();
}
return Ok();
}
As seen above, error handling in ASP has been simplified to a minimum. In most cases, developers only need to remember to add the appropriate attributes to input classes. Often, when inheriting model classes from database models with Entity Framework, most attributes are already added during the database design stage.
Things start to get complicated when specific validation is expected, but even for such cases, appropriate abstract classes have been prepared for implementing custom logic.