Minimal API Validation in .NET 10 - Built-In Support with Data Annotations
Learn how .NET 10 introduces built-in validation for Minimal APIs using Data Annotations - no extra libraries needed
Connect with me:
Want to sponsor this newsletter? Let’s work together →
Introduction
If you’ve ever built a Minimal API in .NET, you probably know the pain - there was no built-in validation out of the box.
You had to rely on third-party libraries like FluentValidation or write manual checks inside your endpoint handlers.
That changes with .NET 10.
Microsoft has introduced native validation support for Minimal APIs using Data Annotations - the same [Required], [MaxLength], and custom validation attributes you already know from MVC controllers.
In this article, I’ll walk you through exactly how it works using a practical Library API example.
🎬 Watch the full video here:
The Problem Before .NET 10
In earlier versions of .NET, Minimal APIs had no automatic model validation. If you defined a POST endpoint like this:
app.MapPost("/books", (CreateBookRequest request) =>
{
// No validation happens automatically!
// You had to manually check or use FluentValidation
});The framework would happily accept any payload - even an empty one - and let your handler deal with it.
This meant:
❌ No automatic
400 Bad Requestresponses❌ No support for
[Required],[MaxLength], etc.❌ Extra libraries or boilerplate validation logic
What’s New in .NET 10
.NET 10 adds a single line that changes everything:
builder.Services.AddValidation();That’s it.
By calling AddValidation() on your service collection, the framework will automatically validate any request model decorated with Data Annotation attributes before your endpoint handler executes.
If validation fails, the API returns a 400 Bad Request with detailed error messages — no manual intervention required.
Full Example - Building a “Library API”
Let’s build a simple Library API that validates book creation requests.
1. Project Setup
Make sure you’re targeting .NET 10:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" />
<PackageReference Include="Scalar.AspNetCore" Version="2.12.30" />
</ItemGroup>
</Project>2. Define the Request Model with Validation
This is where the magic happens.
using System.ComponentModel.DataAnnotations;
namespace LibraryApi;
public record CreateBookRequest(
[Required]
[MinLength(2)]
[MaxLength(200)]
string Title,
[Required]
string Author,
string? Isbn,
[ValidPublishedYear]
int? PublishedYear
);What’s happening here?
Title is required and must be between 2 and 200 characters
Author is required
Isbn is optional
PublishedYear uses a custom validation attribute
3. Create a Custom Validation Attribute
For scenarios where built-in attributes aren’t enough, you can create your own.
using System.ComponentModel.DataAnnotations;
namespace LibraryApi;
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class ValidPublishedYearAttribute : ValidationAttribute
{
private const int MinYear = 1450;
protected override ValidationResult? IsValid(
object? value,
ValidationContext validationContext)
{
if (value is null)
return ValidationResult.Success;
if (value is not int year)
return new ValidationResult("Published year must be a number.");
var currentYear = DateTime.UtcNow.Year;
if (year < MinYear || year > currentYear)
{
return new ValidationResult(
$"Published year must be between {MinYear} and {currentYear}.");
}
return ValidationResult.Success;
}
}Key details
✅
nullis allowed - the field is optional✅ The year must be within a sensible range
✅ Inherits from
ValidationAttribute⚠️
[AttributeUsage(AttributeTargets.Parameter)]is required because record constructor members are parameters, not properties
4. Define the Response DTO
namespace LibraryApi;
public record BookDto(
Guid Id,
string Title,
string Author,
string? Isbn,
int? PublishedYear
);5. Wire Everything in Program.cs
using LibraryApi;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
builder.Services.AddValidation();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
app.MapPost("/books", (CreateBookRequest request) =>
{
var createdBook = new BookDto(
Guid.NewGuid(),
request.Title,
request.Author,
request.Isbn,
request.PublishedYear
);
return Results.Created($"/books/{createdBook.Id}", createdBook);
})
.WithName("CreateBook");
app.Run();📌 The only new line compared to classic Minimal APIs is:
builder.Services.AddValidation();How It Works Under the Hood
When you call AddValidation():
Request arrives → JSON is deserialized into your record
Validation filter runs → Data Annotations are evaluated
Valid → endpoint executes
Invalid → pipeline short-circuits and returns
400 Bad Request
No ModelState, no manual checks, no boilerplate.
Testing the Validation
✅ Valid Request
POST /books
{
"title": "Clean Code",
"author": "Robert C. Martin",
"isbn": "978-0132350884",
"publishedYear": 2008
}Response: 201 Created
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "Clean Code",
"author": "Robert C. Martin",
"isbn": "978-0132350884",
"publishedYear": 2008
}❌ Missing Required Fields
POST /books
{
"isbn": "978-0132350884"
}Response: 400 Bad Request
Validation errors for title and author
❌ Title Too Short
POST /books
{
"title": "A",
"author": "John Doe"
}Response: 400 Bad Request
❌ Future Published Year
POST /books
{
"title": "Future Book",
"author": "Time Traveler",
"publishedYear": 2999
}Response: 400 Bad Request
Key Takeaways
.NET 10 adds first-class validation for Minimal APIs
One line:
builder.Services.AddValidation()Standard Data Annotations just work
Custom validation is fully supported
Automatic 400 responses with Problem Details
No third-party libraries required
This is a major quality-of-life improvement for Minimal APIs.
What used to require FluentValidation or manual checks now works out of the box.
