ASP.NET Core Validation Hataları: ModelState'i { field, message }[] Formatına Dönüştürme
InvalidModelStateResponseFactory ile ModelState hatalarını { field, message }[] formatına normalize edin; böylece istemci tarafında form doğrulama hataları tek tip işlenir.
[ApiController] ile ASP.NET Core, gelen modelleri otomatik olarak doğrular. İstek gövdesi (body) geçersizse,
framework 400 Bad Request ve bir validation payload’ı döndürür. Varsayılan yanıt kullanılabilir olsa da,
errors yapısı genelde bir sözlük (dictionary) şeklindedir (field -> string[]) ve bu da istemci tarafında
ek parse/flatten kodu yazdırır.
Daha temiz bir yaklaşım, validation hatalarını tek bir öngörülebilir formata normalize etmektir:
{ field, message }[]. Böylece UI tarafında (form alanı işaretleme, toast, özet hata listesi vb.)
her zaman aynı dizi (array) üzerinden dönersiniz.
Amaç
- HTTP semantiğini doğru tutmak (validation hataları için
400). - Validation hatalarını tek formatta döndürmek:
{ field, message }[]. - Global çalıştırmak (her controller’da tekrar tekrar kod yazmamak).
- Bonus: Alan adlarını tipik JSON konvansiyonuna uydurmak için camelCase döndürmek.
Varsayılan ModelState Yanıtı (Bugün Ne Geliyor?)
[ApiController] ile geçersiz istekler çoğu zaman şu tarz bir payload üretir (sadeleştirilmiş):
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Email": [ "The Email field is not a valid e-mail address." ],
"Guests": [ "The field Guests must be between 1 and 10." ]
}
}
Sorun status code değil; sorun şekil (shape). İstemci tarafı genellikle bu dictionary’yi flatten edip tek listeye çevirmek zorunda kalır.
Hedef Format (Normalize)
Dictionary yerine basit bir liste dönelim:
{
"message": "Doğrulama başarısız.",
"code": "VALIDATION_ERROR",
"errors": [
{ "field": "email", "message": "The Email field is not a valid e-mail address." },
{ "field": "guests", "message": "The field Guests must be between 1 and 10." }
]
}
Alan adlarının camelCase olduğuna dikkat edin. Bu küçük detay çoğu uygulamada UI tarafındaki eşleştirme işini azaltır.
Adım 1: Küçük bir Error DTO Tanımlayın
public sealed record FieldError(string Field, string Message);
Adım 2: ModelState’i { field, message }[] Formatına Düzleştirin (küçük bir C# 12 dokunuşuyla)
ModelState, hataları dictionary olarak tutar ve her key birden fazla mesaj içerebilir.
Aşağıdaki helper bunu düz (flat) bir listeye çevirir. Bu sırada alan adını camelCase yapmak için
JsonNamingPolicy.CamelCase.ConvertName(...) kullanıyoruz.
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.ModelBinding;
public static class ModelStateExtensions
{
public static List<FieldError> ToFieldErrors(this ModelStateDictionary modelState)
{
var list = new List<FieldError>();
foreach (var kvp in modelState)
{
// Küçük bir C# 12 dokunuşu: alan adlarını camelCase'e normalize et
// ("Email", "Guests" gibi basit key’lerde en iyi sonucu verir)
var field = JsonNamingPolicy.CamelCase.ConvertName(kvp.Key);
foreach (var err in kvp.Value.Errors)
{
// ErrorMessage boşsa Exception mesajına düş (nadir ama mümkün).
var msg = !string.IsNullOrWhiteSpace(err.ErrorMessage)
? err.ErrorMessage
: err.Exception?.Message ?? "Invalid value.";
list.Add(new FieldError(field, msg));
}
}
return list;
}
}
Not: Key’leriniz noktalı path’ler içeriyorsa (ör. request.Email veya items[0].Name),
her segment için camelCase uygulamak isteyebilirsiniz. Birçok API’de basit property adları yeterlidir ve bu yaklaşım da minimal kalır.
Adım 3: Global Validation Yanıtını Override Edin (Program.cs)
En temiz global kanca (hook) ConfigureApiBehaviorOptions’tır.
Model validation fail olunca ASP.NET Core InvalidModelStateResponseFactory çağırır.
Biz de burada normalize edilmiş payload’ı döndürürüz.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errors = context.ModelState.ToFieldErrors();
var payload = new
{
message = "Doğrulama başarısız.",
code = "VALIDATION_ERROR",
errors
};
return new BadRequestObjectResult(payload);
};
});
var app = builder.Build();
app.MapControllers();
app.Run();
Bu noktadan sonra geçersiz tüm istekler otomatik olarak aynı formatta hata listesi döndürür.
Controller içinde tekrar tekrar if (!ModelState.IsValid) yazmanıza gerek kalmaz.
Örnek: DTO + Endpoint
[ApiController] ile ModelState’i manuel kontrol etmenize gerek yoktur.
Model geçersizse, ASP.NET Core bizim factory üzerinden otomatik olarak 400 döndürür.
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
public sealed class CreateReservationRequest
{
[Required, EmailAddress]
public string Email { get; set; } = "";
[Range(1, 10)]
public int Guests { get; set; }
}
[ApiController]
[Route("api/reservations")]
public sealed class ReservationsController : ControllerBase
{
[HttpPost]
public IActionResult Create([FromBody] CreateReservationRequest request)
{
// Geçersizse: 400 otomatik döner (InvalidModelStateResponseFactory üzerinden).
return Ok(new { message = "Created" });
}
}
İstemci Tarafı (Artık Çok Basit)
Validation hataları her zaman liste olarak geldiği için, istemci tarafı hataları tek biçimde gösterebilir:
const res = await fetch("/api/reservations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "not-an-email", guests: 100 })
});
if (!res.ok) {
const payload = await res.json();
// payload.errors her zaman [{ field, message }]
for (const e of payload.errors || []) {
console.log(e.field, e.message);
}
}
Sık Yapılan İyileştirmeler
-
Zaten bir
Result<T>wrapper kullanıyorsanız, burada da aynı konvansiyonları koruyun (aynıcode/errorsyapısı). -
Özellikle production’da faydalı olması için
traceIdekleyin (veya header olarak dönün) ve hataları log ile eşleştirin. -
Noktalı path’lerde camelCase gerekiyorsa, her segmenti ayrı camelCase’e çevirin (ör.
.ile split edin,[0]gibi indeksleri koruyun).
TL;DR
[ApiController]modelleri zaten doğrular ve otomatik400döndürür.- Bu yanıtı global özelleştirmek için
InvalidModelStateResponseFactorykullanın. - ModelState’i
{ field, message }[]formatına düzleştirerek istemci kodunu sadeleştirin. - Varsayılan olarak camelCase alan adı üretmek için
JsonNamingPolicy.CamelCase.ConvertNamekullanın.