Yükleniyor...

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ç


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


TL;DR

  • [ApiController] modelleri zaten doğrular ve otomatik 400 döndürür.
  • Bu yanıtı global özelleştirmek için InvalidModelStateResponseFactory kullanı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.ConvertName kullanın.