ASP.NET Core – API model validation attributes

It’s always a good idea to validate data coming into your web API. There are two steps you can do to guard against invalid data:

  1. Declare your model properties with the proper types. (ex: string, DateTime, int).
  2. Use model validation attributes. The main built-in ones are [Required], [Range], [StringLength], and [RegularExpression].

Here’s an example of using model validation attributes:

using System.ComponentModel.DataAnnotations;

public class Movie
{
	[Required]
	public string Title { get; set; }

	[Required]
	[Range(0.0, 5000.0)]
	public decimal? BoxOfficeMillions { get; set; }

	[Required]
	public DateTime? ReleaseDate { get; set; }
}
Code language: C# (cs)

When a request comes in, the framework does two things:

  • Model binding – It tries to map the request data to the model properties.
  • Model validation – It compares the model values against the model validation attributes.

Let’s say you send a request with invalid data (boxOfficeMillions is outside of the specified range):

{
    "title": "The Matrix",
    "releaseDate":"1999-03-31",
    "boxOfficeMillions": -1
}
Code language: JSON / JSON with Comments (json)

It successfully does the model binding step, but then it finds invalid data during the model validation step. By default, if you’re using the [ApiController] attribute on your API controller, then it’ll automatically return a problem details error response (400 – Bad Request) with the following error details:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMHEO7OR788U:00000002",
    "errors": {
        "BoxOfficeMillions": [
            "The field BoxOfficeMillions must be between 0 and 5000."
        ]
    }
}
Code language: JSON / JSON with Comments (json)

Note: If you’re not using the [ApiController] attribute, and you want to manually return an error, you can get the error info from the ModelState object.

As you can see, it generates a very specific error message containing the property name and why it failed (which is based on the model validation attribute that was used).

In this article, I’ll show examples of how to use the main built-in model validation attributes – [Required], [Range], [StringLength], and [RegularExpression]. If these don’t solve your problem, you can write a custom model validation attribute.

Overriding the error message

The default error message is usually good enough. However, every attribute has an optional ErrorMessage parameter that allows you to override the error message. Here’s an example:

[Required]
[Range(0.0, 5000.0, ErrorMessage = "BoxOfficeMillions must be between $0 and $5000 million ($5 billion)" )]
public decimal? BoxOfficeMillions { get; set; }
Code language: C# (cs)

This changes the error message for that property in the error response:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMHEOPKUJ2L4:00000001",
    "errors": {
        "BoxOfficeMillions": [
            "BoxOfficeMillions must be between $0 and $5000 million ($5 billion)"
        ]
    }
}
Code language: JSON / JSON with Comments (json)

Note: You can use format placeholders in the error message (i.e. ErrorMessage = “{0} is required”), but I don’t recommend using that feature since it makes your code a bit fragile.

[Required] attribute

Use the [Required] attribute to verify that a nullable property has a value. It will fail validation if the property is missing, the value is null, or if its an empty string. Here’s an example:

using System.ComponentModel.DataAnnotations;

public class Movie
{
	[Required]
	public int? Id { get; set; }

	[Required]
	public string Title { get; set; }
}
Code language: C# (cs)

Now let’s say you send in the following invalid data (id is null and title is missing):

{
    "id":null
}Code language: JSON / JSON with Comments (json)

This produces the following error response. Notice that it shows all of the validation errors:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMHEP1U0GPL4:00000001",
    "errors": {
        "Id": [
            "The Id field is required."
        ],
        "Title": [
            "The Title field is required."
        ]
    }
}
Code language: JSON / JSON with Comments (json)

Use with nullable properties

The [Required] attribute only works on nullable properties because model binding happens before model validation. During model binding, if a non-nullable property is missing in the request data, then the model property is initialized with the type’s default value (ex: 0 for int). Hence, when it then does model validation, it sees a non-null value and therefore passes validation.

Therefore, if you want to use the [Required] attribute, make sure to use nullable types. For example:

using System.ComponentModel.DataAnnotations;

public class Movie
{
	[Required]
	public int? Id { get; set; }

	[Required]
	public string Title { get; set; }
	
	[Required]
	public DateTime? ReleaseDate { get; set; }
}
Code language: C# (cs)

This is using string because it’s already nullable. It’s using a nullable int (int?) and a nullable DateTime (DateTime?).

Note: This isn’t the same as sending a null for a non-nullable property. In that scenario, it results in a model binding error.

Empty strings

Strings are a special case. By default, [Required] will return an error if a string is null or empty. In other words, the following request with an empty title property will produce an error:

{
    "title":"",
    "id":1
}Code language: JSON / JSON with Comments (json)

Sometimes you may want to allow empty strings as a valid value, while still rejecting null values. To do that, set AllowEmptyStrings to true:

[Required(AllowEmptyStrings = true)]
public string Title { get; set; }
Code language: C# (cs)

[Range] attribute

Use the [Range] attribute to verify the property’s value is between a min and max. This is mostly used for numeric types (ex: int, decimal), but can also be used with any type that implements IComparable (ex: DateTime). I’ll show examples below.

Number is between min and max

Here’s an example of verifying that a numeric value (integer in this case) is between a min and max value:

using System.ComponentModel.DataAnnotations;

public class Movie
{
	[Required]
	[Range(1, 10000)]
	public int? Id { get; set; }

	[Required]
	public string Title { get; set; }
}
Code language: C# (cs)

Now send invalid data (id is outside of the specified range):

{
    "title":"The Matrix",
    "id":0
}
Code language: JSON / JSON with Comments (json)

This produces the following error response:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMHEQ01EL6O8:00000001",
    "errors": {
        "Id": [
            "The field Id must be between 1 and 10000."
        ]
    }
}
Code language: JSON / JSON with Comments (json)

DateTime is between two dates

The [Range] attribute can also be used with non-numeric types. You can use it with any type that implements IComparable. You have to provide the type and the min/max values as strings.

Here’s an example of verifying that a DateTime property is within a date range:

using System.ComponentModel.DataAnnotations;

public class Movie
{
	[Required]
	public string Title { get; set; }

	[Required]
	[Range(typeof(DateTime), minimum: "2000-01-01", maximum: "2050-01-01" )]
	public DateTime? ReleaseDate { get; set; }
}
Code language: C# (cs)

Notice that the min/max are specified as DateTime strings.

Now send it invalid data (the releaseDate is before the 2000-01-01 minimum):

{
    "title":"The Matrix",
    "releaseDate":"1999-03-31"
}
Code language: JSON / JSON with Comments (json)

This produces the following error response:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMHEQ4UNU9LI:00000003",
    "errors": {
        "ReleaseDate": [
            "The field ReleaseDate must be between 1/1/2000 12:00:00 AM and 1/1/2050 12:00:00 AM."
        ]
    }
}
Code language: JSON / JSON with Comments (json)

Length attributes

There are three length attributes: [MinLength], [MaxLength], and [StringLength]. The most common scenario is needing to set a max length for a string. It’s preferable to use the [StringLength] attribute for this, because it has a better default error message.

Here’s an example of using [StringLength] to limit the max length of a string:

using System.ComponentModel.DataAnnotations;
	
public class Movie
{
	[Required]
	[StringLength(13)]
	public string Id { get; set; }

	[Required]
	public string Title { get; set; }
}
Code language: C# (cs)

Note: You can set a length range for a string like this [StringLength(100, MinimumLength = 50)]. This is better than needing to use two attributes – [MinLength(50)] and [MaxLength(100)].

Now send a request with invalid data (the id has more than 13 characters):

{
    "title":"The Matrix",
    "id":"12345678901234"
}
Code language: JSON / JSON with Comments (json)

This produces the following error response:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMHEQJFQO9SK:00000001",
    "errors": {
        "Id": [
            "The field Id must be a string with a maximum length of 13."
        ]
    }
}
Code language: JSON / JSON with Comments (json)

[RegularExpression] attribute

When you need to validate that a property’s value matches a pattern, one option is to use the [RegularExpression] attribute. Regex is usually difficult to get right and is much slower than iterative approaches, so I’d recommend the following:

  • Figure out your regex pattern with a tool (like regexstorm.net).
  • Or find a well-known and tested pattern (search regexr.com).
  • Thoroughly test. Regex problems surface as runtime exceptions.
  • Use an existing special-purpose model validation attribute if possible (ex: [Phone], [EmailAddress]).
  • Write your own custom model validation attribute (not shown here).
  • Override the default error message with an example of valid input. The default error message shows the regex pattern, which is really unfriendly.

With that said, here’s an example of using the [RegularExpression] attribute to validate a relatively simple pattern:

using System.ComponentModel.DataAnnotations;

public class Movie
{
	[Required]
	[RegularExpression("[A-Z]{3}[0-9]{3}", ErrorMessage = "Doesn't match pattern. Valid example: ABC123")]
	public string Id { get; set; }

	[Required]
	public string Title { get; set; }
}
Code language: C# (cs)

Now send a request with invalid data (id doesn’t match the pattern):

{
    "title":"The Matrix",
    "id":"123"
}
Code language: JSON / JSON with Comments (json)

This produces the following error response (with the custom error message):

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMHEQP307PDU:00000004",
    "errors": {
        "Id": [
            "Doesn't match pattern. Valid example: ABC123"
        ]
    }
}
Code language: JSON / JSON with Comments (json)

Leave a Comment