Core API Model Validation
Model validation ensures that data sent by the client meets the rules defined in the model before the action method processes it. Without validation, a book with no title, a negative price, or a missing author could be saved to the database. Validation catches these problems early and returns clear error messages to the client.
Why Validation Matters
Consider this request sent to the BookStore API:
POST /api/books
{
"title": "",
"author": "",
"price": -5,
"category": "Technology"
}
Without validation, this invalid data gets stored in the database. With validation, the API rejects it immediately with a clear error message explaining what is wrong.
Data Annotations – Built-in Validation Attributes
ASP.NET Core uses Data Annotations — attributes applied directly to model properties — to define validation rules. These are in the System.ComponentModel.DataAnnotations namespace.
// Models/Book.cs
using System.ComponentModel.DataAnnotations;
namespace BookStoreAPI.Models
{
public class Book
{
public int Id { get; set; }
[Required(ErrorMessage = "Title is required.")]
[StringLength(200, MinimumLength = 2,
ErrorMessage = "Title must be between 2 and 200 characters.")]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "Author is required.")]
[StringLength(100, MinimumLength = 2,
ErrorMessage = "Author must be between 2 and 100 characters.")]
public string Author { get; set; } = string.Empty;
[Required(ErrorMessage = "Price is required.")]
[Range(0.01, 9999.99, ErrorMessage = "Price must be between 0.01 and 9999.99.")]
public decimal Price { get; set; }
[Required(ErrorMessage = "Category is required.")]
public string Category { get; set; } = string.Empty;
public bool IsAvailable { get; set; }
public DateTime CreatedDate { get; set; }
}
}
Common Validation Attributes
| Attribute | Purpose | Example |
|---|---|---|
[Required] | Field cannot be null or empty | [Required] |
[StringLength] | Min and max string length | [StringLength(100, MinimumLength = 2)] |
[Range] | Numeric range (min, max) | [Range(0.01, 9999.99)] |
[MinLength] | Minimum string length | [MinLength(2)] |
[MaxLength] | Maximum string length | [MaxLength(200)] |
[EmailAddress] | Valid email format | [EmailAddress] |
[Url] | Valid URL format | [Url] |
[RegularExpression] | Matches a regex pattern | [RegularExpression(@"^\d{4}$")] |
[Compare] | Two fields must match | [Compare("Password")] |
How [ApiController] Handles Validation Automatically
When the controller has [ApiController], ASP.NET Core checks ModelState automatically before the action method runs. If validation fails, it returns a 400 Bad Request with a detailed error response — without any extra code in the action method.
POST /api/books
{
"title": "",
"price": -5
}
Response: 400 Bad Request
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Title": ["Title is required."],
"Author": ["Author is required."],
"Price": ["Price must be between 0.01 and 9999.99."],
"Category": ["Category is required."]
}
}
Manual ModelState Checking
Without [ApiController], or when custom control is needed, ModelState.IsValid can be checked manually inside the action method:
[HttpPost]
public IActionResult Create([FromBody] Book book)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
book.Id = _books.Any() ? _books.Max(b => b.Id) + 1 : 1;
book.CreatedDate = DateTime.Now;
_books.Add(book);
return CreatedAtAction(nameof(GetById), new { id = book.Id }, book);
}
Custom Validation with IValidatableObject
Sometimes a business rule cannot be expressed with a simple attribute. For example, in the BookStore API, a rule might be: "If the category is 'Rare', the price must be at least 100." This can be implemented by making the model implement IValidatableObject.
// Models/Book.cs
using System.ComponentModel.DataAnnotations;
public class Book : IValidatableObject
{
[Required]
public string Title { get; set; } = string.Empty;
[Required]
public string Author { get; set; } = string.Empty;
[Range(0.01, 9999.99)]
public decimal Price { get; set; }
[Required]
public string Category { get; set; } = string.Empty;
public bool IsAvailable { get; set; }
public DateTime CreatedDate { get; set; }
// Custom validation rule
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Category == "Rare" && Price < 100)
{
yield return new ValidationResult(
"Books in the 'Rare' category must cost at least 100.",
new[] { nameof(Price) }
);
}
}
}
Custom Validation Attributes
Reusable custom rules can be packaged as a custom attribute. The example below creates a [FutureDate] attribute that ensures a date is in the future:
// ValidationAttributes/FutureDateAttribute.cs
using System.ComponentModel.DataAnnotations;
public class FutureDateAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(
object? value, ValidationContext validationContext)
{
if (value is DateTime date && date <= DateTime.Now)
{
return new ValidationResult("Date must be in the future.");
}
return ValidationResult.Success;
}
}
Usage on a model property:
[FutureDate]
public DateTime PublishDate { get; set; }
Validation Flow Diagram
Client sends POST /api/books with JSON body
|
v
[ Model Binding ] → Deserialize JSON → Book object
|
v
[ Validation ] → Check all [Required], [Range], [StringLength]...
|
├── Validation FAILS?
│ → Return 400 Bad Request + error details
│
└── Validation PASSES?
→ Action method runs
→ Book saved
→ Return 201 Created
Validation Best Practices for the BookStore API
| Field | Rule | Attribute |
|---|---|---|
| Title | Required, 2–200 characters | [Required][StringLength(200, MinimumLength=2)] |
| Author | Required, 2–100 characters | [Required][StringLength(100, MinimumLength=2)] |
| Price | Required, 0.01 to 9999.99 | [Required][Range(0.01, 9999.99)] |
| Category | Required | [Required] |
| IsAvailable | No validation needed (bool default is false) | — |
| CreatedDate | Set by server, not from client | — |
Key Points
- Data Annotations like
[Required],[StringLength], and[Range]define validation rules directly on model properties. - With
[ApiController], validation runs automatically before the action method — no extra code needed. - Failed validation returns 400 Bad Request with a detailed error structure.
- Complex rules that span multiple fields are handled with
IValidatableObject. - Reusable custom rules are created as custom validation attributes by extending
ValidationAttribute.
