Minimal APIs shipped without built-in validation. For years, you needed FluentValidation or manual checks. .NET 10 finally adds native validation support with AddValidation().
Common questions this answers
- How do I enable validation in Minimal APIs?
- What validation attributes are supported?
- How do I create custom validators?
- How do I customize error responses?
- How does this compare to FluentValidation?
Definition (what this means in practice)
AddValidation() registers validation services that automatically validate request models in Minimal API endpoints. When a request fails validation, the runtime returns a 400 Bad Request with ProblemDetails containing the errors.
In practice, this means you can add [Required], [Range], and other data annotations to your models, and validation happens automatically without manual checks in every endpoint.
Terms used
- Data annotations: attributes from System.ComponentModel.DataAnnotations for declarative validation.
- ProblemDetails: RFC 7807 standard format for HTTP API error responses.
- Interceptors: compile-time source generators that add validation logic without runtime reflection.
- IValidatableObject: interface for custom validation logic with access to the entire object.
Reader contract
This article is for:
- Engineers adding validation to Minimal API endpoints.
- Teams migrating from manual validation or FluentValidation.
You will leave with:
- Working AddValidation() configuration.
- Custom validator patterns.
- Error response customization.
- Decision guidance for built-in vs FluentValidation.
This is not for:
- MVC controller validation (different system, already built-in).
- Client-side validation.
- Blazor validation.
Quick start (10 minutes)
If you want automatic validation in Minimal APIs:
Verified on: ASP.NET Core (.NET 10).
1. Add the project file configuration:
<!-- In your .csproj file -->
<PropertyGroup>
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Validation.Generated</InterceptorsNamespaces>
</PropertyGroup>
2. Register validation services:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidation();
var app = builder.Build();
3. Add validation attributes to your model:
public record CreateProductRequest(
[Required] string Name,
[StringLength(500)] string Description,
[Range(0.01, 10000)] decimal Price);
4. Use the model in an endpoint:
app.MapPost("/products", (CreateProductRequest request) =>
{
// Validation happens automatically before this code runs
return TypedResults.Created($"/products/{Guid.NewGuid()}", request);
});
Invalid requests now return 400 Bad Request with error details automatically.
How AddValidation works
When you call AddValidation(), ASP.NET Core:
- Discovers types used in Minimal API handlers at compile time.
- Generates interceptors that add validation logic.
- Validates models during request binding.
- Returns ProblemDetails on validation failure.
This approach uses source generators instead of runtime reflection, improving startup performance.
Supported validation attributes
All attributes from System.ComponentModel.DataAnnotations work:
| Attribute | Purpose |
|---|---|
| [Required] | Field must have a value |
| [StringLength] | Maximum (and optional minimum) string length |
| [MinLength], [MaxLength] | Collection or string length bounds |
| [Range] | Numeric value bounds |
| [RegularExpression] | Pattern matching |
| [EmailAddress] | Email format validation |
| [Phone] | Phone number format |
| [Url] | URL format |
| [Compare] | Cross-property comparison |
| [CreditCard] | Credit card number format |
Example with multiple attributes
public record RegisterUserRequest(
[Required]
[EmailAddress]
string Email,
[Required]
[StringLength(100, MinimumLength = 8)]
string Password,
[Required]
[Compare(nameof(Password))]
string ConfirmPassword,
[Phone]
string? PhoneNumber);
Validation on different binding sources
Validation applies to all binding sources:
Request body
app.MapPost("/products", (CreateProductRequest request) => ...);
Query parameters
app.MapGet("/search", ([Required] string query, [Range(1, 100)] int limit = 10) => ...);
Route parameters
app.MapGet("/products/{id}", ([Range(1, int.MaxValue)] int id) => ...);
Headers
app.MapGet("/secure", ([Required][FromHeader(Name = "X-Api-Key")] string apiKey) => ...);
Record types
Records work identically to classes. Apply attributes in the primary constructor:
public record OrderRequest(
[Required] string CustomerId,
[Required] [MinLength(1)] List<OrderItem> Items,
[Range(0, 1000)] decimal Discount);
public record OrderItem(
[Required] string ProductId,
[Range(1, 100)] int Quantity);
Nested types are validated recursively.
Custom validation attributes
Create custom attributes by inheriting from ValidationAttribute:
public class FutureDateAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext context)
{
if (value is DateOnly date && date <= DateOnly.FromDateTime(DateTime.Today))
{
return new ValidationResult(
ErrorMessage ?? "Date must be in the future.",
[context.MemberName!]);
}
return ValidationResult.Success;
}
}
Use it like any other attribute:
public record CreateEventRequest(
[Required] string Title,
[FutureDate] DateOnly EventDate);
IValidatableObject for complex validation
When validation depends on multiple properties, implement IValidatableObject:
public record DateRangeRequest(
DateOnly StartDate,
DateOnly EndDate) : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (EndDate < StartDate)
{
yield return new ValidationResult(
"End date must be after start date.",
[nameof(EndDate)]);
}
if ((EndDate.ToDateTime(TimeOnly.MinValue) - StartDate.ToDateTime(TimeOnly.MinValue)).Days > 365)
{
yield return new ValidationResult(
"Date range cannot exceed one year.",
[nameof(StartDate), nameof(EndDate)]);
}
}
}
IValidatableObject runs after attribute validation passes.
Error response format
Failed validation returns a 400 Bad Request with ProblemDetails:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": ["The Name field is required."],
"Price": ["The field Price must be between 0.01 and 10000."]
}
}
This format follows RFC 7807 and is consistent with ASP.NET Core's standard error responses.
Customizing error responses
Use IProblemDetailsService to customize validation error responses:
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Instance = context.HttpContext.Request.Path;
context.ProblemDetails.Extensions["traceId"] = context.HttpContext.TraceIdentifier;
};
});
For more control, implement a custom IProblemDetailsService:
public class CustomProblemDetailsService : IProblemDetailsService
{
public ValueTask WriteAsync(ProblemDetailsContext context)
{
var problemDetails = context.ProblemDetails;
// Customize error format
problemDetails.Title = "Validation Failed";
problemDetails.Extensions["timestamp"] = DateTime.UtcNow;
context.HttpContext.Response.StatusCode = problemDetails.Status ?? 400;
return new ValueTask(
context.HttpContext.Response.WriteAsJsonAsync(problemDetails));
}
}
builder.Services.AddSingleton<IProblemDetailsService, CustomProblemDetailsService>();
Disabling validation for specific endpoints
Use DisableValidation() when you intentionally accept incomplete data:
// Partial update endpoint - validation handled manually
app.MapPatch("/products/{id}", (int id, JsonPatchDocument<Product> patch) =>
{
// Manual validation logic for patches
return TypedResults.Ok();
}).DisableValidation();
// Internal endpoint with trusted input
app.MapPost("/internal/sync", (SyncRequest request) =>
{
return TypedResults.Ok();
}).DisableValidation();
Nullable value types in forms
Empty form fields map to null for nullable types instead of causing parse failures:
public record UpdateProductRequest(
string? Name,
decimal? Price, // Empty string becomes null
DateOnly? ExpiryDate // Empty string becomes null
);
This prevents validation errors when optional fields are left empty in HTML forms.
Built-in validation vs FluentValidation
| Criterion | AddValidation() | FluentValidation |
|---|---|---|
| Setup complexity | Minimal (one line) | NuGet + registration |
| Validation location | Attributes on model | Separate validator classes |
| Complex rules | IValidatableObject | Fluent syntax |
| Async validation | Not supported | Supported |
| Conditional rules | Manual in IValidatableObject | Built-in When/Unless |
| Testability | Test the model | Test the validator |
| Learning curve | Familiar if using MVC | New API to learn |
When to use AddValidation()
- Simple to moderate validation requirements.
- Team familiar with data annotations.
- Validation rules are straightforward.
- You want minimal dependencies.
When to consider FluentValidation
- Complex conditional validation logic.
- Async validation (database checks).
- Validation rules change frequently.
- You prefer validation logic separate from models.
Copy/paste artifact: validation configuration
// Program.cs - Complete validation setup
var builder = WebApplication.CreateBuilder(args);
// Register validation services
builder.Services.AddValidation();
// Optional: customize error responses
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Extensions["traceId"] = context.HttpContext.TraceIdentifier;
};
});
var app = builder.Build();
// Endpoint with automatic validation
app.MapPost("/products", (CreateProductRequest request) =>
{
return TypedResults.Created($"/products/{Guid.NewGuid()}", request);
});
// Endpoint with validation disabled
app.MapPatch("/products/{id}", (int id, UpdateProductRequest request) =>
{
return TypedResults.Ok();
}).DisableValidation();
app.Run();
// Models with validation
public record CreateProductRequest(
[Required]
[StringLength(200)]
string Name,
[StringLength(2000)]
string? Description,
[Required]
[Range(0.01, 100000)]
decimal Price);
public record UpdateProductRequest(
string? Name,
string? Description,
decimal? Price);
Required .csproj addition:
<PropertyGroup>
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Validation.Generated</InterceptorsNamespaces>
</PropertyGroup>
Common failure modes
Missing InterceptorsNamespaces: validation silently does not run. Add the PropertyGroup to your .csproj file.
Forgetting AddValidation(): models are not validated. Ensure the service is registered.
Nullable reference warnings: use nullable types for optional fields to avoid false validation errors.
Nested type validation not running: ensure nested types also have validation attributes; validation is recursive but only for annotated properties.
Custom attribute not working: ensure your attribute inherits from ValidationAttribute and implements IsValid correctly.
Checklist
- InterceptorsNamespaces added to .csproj.
- AddValidation() called in Program.cs.
- Models use data annotation attributes.
- Nullable types used for optional fields.
- Custom validators inherit from ValidationAttribute.
- IValidatableObject used for cross-property rules.
- DisableValidation() used intentionally where needed.
FAQ
Does validation work with dependency injection?
Custom validators via IValidatableObject can access services through ValidationContext.GetService(). Custom attributes do not have direct DI access.
Can I validate async (like checking database uniqueness)?
No. Built-in validation is synchronous. For async validation, use FluentValidation or validate manually in the endpoint.
What happens if validation and binding both fail?
Binding errors take precedence. If a value cannot be bound (wrong type), validation does not run for that property.
Is the validation filter order configurable?
The validation filter runs during model binding before your endpoint code. The order is not configurable.
Do I need to check ModelState in Minimal APIs?
No. Unlike MVC controllers, Minimal APIs with AddValidation() automatically return 400 on validation failure. Your endpoint code only runs if validation passes.
What to do next
If you have existing Minimal APIs with manual validation, try AddValidation() on a new endpoint. Compare the code reduction and error response consistency.
For more on Minimal APIs architecture decisions, read Minimal APIs vs Controllers: A Decision Framework.
If you want help implementing validation patterns in your application, reach out via Contact.
References
- What's new in ASP.NET Core 10 - Validation
- Minimal APIs overview
- Validation support in Minimal APIs
- Customize validation error responses using IProblemDetailsService
- System.ComponentModel.DataAnnotations
Author notes
Decisions:
- Focus on built-in validation rather than FluentValidation tutorial. Rationale: built-in is the new default; FluentValidation is well-documented elsewhere.
- Include IValidatableObject for complex scenarios. Rationale: fills the gap for cross-property validation without external dependencies.
- Note async validation limitation. Rationale: important for teams evaluating whether built-in validation meets their needs.
Observations:
- Teams often add FluentValidation first, then discover AddValidation() is sufficient for most cases.
- Missing InterceptorsNamespaces is the most common setup issue.
- IValidatableObject is underutilized; many teams do not know it works with Minimal APIs.