← Kursa Dön
📄 Text · 15 min

Content Negotiation

Content Negotiation (İçerik Pazarlığı), HTTP protokolünde istemci ve sunucunun, yanıtın hangi formatta (JSON, XML, HTML vb.) döneceği konusunda anlaşma mekanizmasıdır. Bir REST API'den aynı kaynak için hem JSON hem de XML yanıt almak isteyebilirsiniz — bu işlem content negotiation ile gerçekleşir.

Bir restoranda menü sipariş ederken "aynı yemek ama tabakta mı, paket mi?" diye sormaya benzer. Yemek (veri) aynıdır, sadece sunum şekli (format) değişir. İstemci Accept header ile "bana JSON ver" veya "bana XML ver" der, sunucu da uygun formatta yanıt döner.

HTTP Content Negotiation Temelleri

Content negotiation HTTP/1.1 spesifikasyonunun (RFC 7231) bir parçasıdır. İstemci ve sunucu arasında şu anlaşma mekanizması çalışır:

  1. İstemciAccept header ile hangi formatı istediğini bildirir

  2. Sunucu → İsteği karşılayabilecek en uygun formatı seçer

  3. SunucuContent-Type header ile yanıtın formatını bildirir

Bu mekanizma sunucu-taraflı (server-driven) content negotiation olarak adlandırılır.

Accept Header ile Content Negotiation

HTTP'de content negotiation'ın en yaygın ve standart yöntemi Accept header'ıdır. İstemci, hangi formatta yanıt beklediğini bu header ile belirtir:

# JSON istiyorum
GET /api/users/42 HTTP/1.1
Accept: application/json

# XML istiyorum
GET /api/users/42 HTTP/1.1
Accept: application/xml

# JSON tercih ederim, yoksa XML de olur
GET /api/users/42 HTTP/1.1
Accept: application/json, application/xml;q=0.9

# Her formatı kabul ederim
GET /api/users/42 HTTP/1.1
Accept: */*

# JSON tercihim en yüksek, XML ikinci, text üçüncü
GET /api/users/42 HTTP/1.1
Accept: application/json;q=1.0, application/xml;q=0.8, text/plain;q=0.5

q parametresi (quality factor) tercih sırasını belirtir. 0 ile 1 arasında değer alır, varsayılan 1'dir. Yüksek q değeri daha yüksek tercih anlamına gelir.

Accept Header Parsing

Spring MVC, Accept header'ını parse ederken şu kuralları uygular:

Accept: application/json;q=1.0, application/xml;q=0.8, */*;q=0.5

Tercih sırası:
1. application/json (q=1.0) — En yüksek tercih
2. application/xml (q=0.8) — İkinci tercih
3. */* (q=0.5) — Her format kabul edilir ama düşük tercih

Sunucu, istemcinin tercihlerini ve kendi yeteneklerini karşılaştırarak en uygun formatı seçer. Hiçbir format eşleşmezse 406 Not Acceptable yanıtı döner.

Spring MVC'de Content Negotiation

Spring MVC, content negotiation'ı otomatik olarak destekler. Yapmanız gereken sadece ilgili kütüphaneleri eklemek ve controller'ı yapılandırmaktır:

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    // Accept: application/json → JSON döner
    // Accept: application/xml  → XML döner (XML desteği ekliyse)
    @GetMapping(value = "/{id}",
        produces = {MediaType.APPLICATION_JSON_VALUE,
                    MediaType.APPLICATION_XML_VALUE})
    public UserDto getUser(@PathVariable Long id) {
        return userService.findById(id);
    }

    // Sadece JSON — XML istekleri 406 Not Acceptable alır
    @GetMapping(value = "/json-only/{id}",
        produces = MediaType.APPLICATION_JSON_VALUE)
    public UserDto getUserJsonOnly(@PathVariable Long id) {
        return userService.findById(id);
    }
}

JSON ve XML Desteği

Spring Boot varsayılan olarak JSON desteği (Jackson) ile gelir. XML desteği eklemek için jackson-dataformat-xml dependency'si gerekir:

<!-- pom.xml — XML desteği ekle -->
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

Bu dependency eklendikten sonra, aynı endpoint hem JSON hem XML döndürebilir. Hiçbir kod değişikliği gerekmez — Jackson otomatik olarak her iki formatta serializasyon yapar:

// Accept: application/json → JSON
{
  "id": 42,
  "name": "Ahmet",
  "email": "ahmet@mail.com",
  "age": 25
}
<!-- Accept: application/xml → XML -->
<UserDto>
  <id>42</id>
  <name>Ahmet</name>
  <email>ahmet@mail.com</email>
  <age>25</age>
</UserDto>

XML Annotation'ları ile Özelleştirme

Jackson XML modülü, JAXB annotation'larını da destekler:

@JacksonXmlRootElement(localName = "user")
public class UserDto {
    
    @JacksonXmlProperty(isAttribute = true)
    private Long id;
    
    @JacksonXmlProperty(localName = "full-name")
    private String name;
    
    private String email;
    
    @JacksonXmlElementWrapper(localName = "roles")
    @JacksonXmlProperty(localName = "role")
    private List<String> roles;
}

XML çıktısı:

<user id="42">
  <full-name>Ahmet</full-name>
  <email>ahmet@mail.com</email>
  <roles>
    <role>USER</role>
    <role>ADMIN</role>
  </roles>
</user>

produces ve consumes ile Kısıtlama

produces ve consumes özellikleri, bir endpoint'in hangi formatları desteklediğini sınırlar:

@RestController
@RequestMapping("/api/data")
public class DataController {

    // Sadece JSON kabul eder, sadece JSON döndürür
    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE,
                 produces = MediaType.APPLICATION_JSON_VALUE)
    public DataResponse processJson(@RequestBody DataRequest request) {
        return dataService.process(request);
    }

    // Sadece XML kabul eder, sadece XML döndürür
    @PostMapping(value = "/xml",
        consumes = MediaType.APPLICATION_XML_VALUE,
        produces = MediaType.APPLICATION_XML_VALUE)
    public DataResponse processXml(@RequestBody DataRequest request) {
        return dataService.process(request);
    }

    // JSON veya XML kabul eder, aynı formatta döndürür
    @PostMapping(value = "/multi",
        consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE},
        produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
    public DataResponse processMulti(@RequestBody DataRequest request) {
        return dataService.process(request);
    }
}

Uyumsuzluk durumlarında:

  • İstemci Content-Type: application/xml ile consumes = "application/json" endpoint'ine istek gönderirse → 415 Unsupported Media Type

  • İstemci Accept: text/html ile produces = "application/json" endpoint'ine istek gönderirse → 406 Not Acceptable

⚠️ Dikkat: produces ve consumes belirtilmezse endpoint tüm formatları kabul eder ve döndürür (classpath'teki converter'lara bağlı olarak). Açıkça belirtmek API sözleşmenizi net tutar.

ContentNegotiationConfigurer ile Global Yapılandırma

Varsayılan content negotiation stratejisini özelleştirebilirsiniz:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            // Varsayılan content type — Accept header yoksa bu kullanılır
            .defaultContentType(MediaType.APPLICATION_JSON)

            // Accept header'ı kullanarak negotiation
            .favorParameter(false)       // ?format=json parametresini devre dışı bırak
            .ignoreAcceptHeader(false)   // Accept header'ı dikkate al

            // Desteklenen media type'lar
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML)
            .mediaType("csv", MediaType.parseMediaType("text/csv"));
    }
}

Query Parameter ile Format Seçimi

Accept header yerine query parameter ile format belirleme de yaygın bir yaklaşımdır. Özellikle tarayıcıdan doğrudan test ederken kullanışlıdır (tarayıcı Accept header'ını kontrol etmek zordur):

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer
            .favorParameter(true)        // ?format=xxx parametresini etkinleştir
            .parameterName("format")     // Parametre adı
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("json", MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML);
    }
}
GET /api/users/42?format=json  → JSON yanıt
GET /api/users/42?format=xml   → XML yanıt
GET /api/users/42              → JSON (varsayılan)

Custom MediaType ile API Versiyonlama

Özel API sürümleme veya format tanımlamak için custom media type'lar kullanabilirsiniz. Bu, API versiyonlama için en zarif yöntemlerden biridir:

// Custom MediaType sabitleri
public class CustomMediaType {
    public static final String V1_JSON = "application/vnd.myapp.v1+json";
    public static final String V2_JSON = "application/vnd.myapp.v2+json";
    public static final MediaType V1_JSON_TYPE = MediaType.parseMediaType(V1_JSON);
    public static final MediaType V2_JSON_TYPE = MediaType.parseMediaType(V2_JSON);
}

@RestController
@RequestMapping("/api/users")
public class UserController {

    // Accept: application/vnd.myapp.v1+json → V1 yanıt
    @GetMapping(value = "/{id}", produces = CustomMediaType.V1_JSON)
    public UserV1Dto getUserV1(@PathVariable Long id) {
        User user = userService.findById(id);
        return new UserV1Dto(user.getId(), user.getName(), user.getEmail());
    }

    // Accept: application/vnd.myapp.v2+json → V2 yanıt (ek alanlar)
    @GetMapping(value = "/{id}", produces = CustomMediaType.V2_JSON)
    public UserV2Dto getUserV2(@PathVariable Long id) {
        User user = userService.findById(id);
        return new UserV2Dto(user.getId(), user.getName(), user.getEmail(),
            user.getAvatar(), user.getCreatedAt(), user.getRoles());
    }
}

Bu yaklaşım, GitHub API'si gibi büyük platformlar tarafından kullanılır. URL'yi değiştirmeden (/api/v1/users vs /api/v2/users) aynı URL'den farklı versiyonlara erişim sağlar.

💡 İpucu: vnd. prefix'i "vendor" anlamına gelir — özel, vendor-specific bir media type olduğunu belirtir. +json suffix'i ise JSON formatında olduğunu söyler. Bu format IANA standartlarına uygundur.

HttpMessageConverter — Dönüşüm Zinciri

Spring MVC, content negotiation'ı HttpMessageConverter zinciri aracılığıyla uygular. Her converter belirli bir media type'ı destekler:

Converter                                    → Media Type
────────────────────────────────────────────────────────────
MappingJackson2HttpMessageConverter          → application/json
MappingJackson2XmlHttpMessageConverter       → application/xml
StringHttpMessageConverter                   → text/plain
ByteArrayHttpMessageConverter                → application/octet-stream
FormHttpMessageConverter                     → application/x-www-form-urlencoded
ResourceHttpMessageConverter                 → */* (dosya indirme)

Spring MVC, isteğe uygun converter'ı seçerken şu süreci izler:

  1. İstemcinin Accept header'ını parse et

  2. Controller'ın produces değerini kontrol et

  3. Kayıtlı converter'lar arasında uyumlu olanı bul

  4. En yüksek q değerine sahip eşleşmeyi seç

  5. Converter ile nesneyi serialize et

Custom HttpMessageConverter

Özel formatlar için kendi converter'ınızı yazabilirsiniz:

// CSV format desteği ekleyen custom converter
public class CsvHttpMessageConverter extends AbstractHttpMessageConverter<List<?>> {

    public CsvHttpMessageConverter() {
        super(MediaType.parseMediaType("text/csv"));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return List.class.isAssignableFrom(clazz);
    }

    @Override
    protected List<?> readInternal(Class<? extends List<?>> clazz,
                                    HttpInputMessage inputMessage) {
        // CSV → List dönüşümü
        throw new UnsupportedOperationException("CSV okuma henüz desteklenmiyor");
    }

    @Override
    protected void writeInternal(List<?> list, HttpOutputMessage outputMessage)
            throws IOException {
        // List → CSV dönüşümü
        OutputStreamWriter writer = new OutputStreamWriter(
            outputMessage.getBody(), StandardCharsets.UTF_8);
        
        for (Object item : list) {
            // Basit CSV yazımı (gerçek projede OpenCSV kullanın)
            writer.write(item.toString());
            writer.write("\n");
        }
        writer.flush();
    }
}

// Kayıt
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new CsvHttpMessageConverter());
    }
}

Artık Accept: text/csv ile istek gönderildiğinde veriler CSV formatında döner:

@GetMapping(value = "/export",
    produces = {"application/json", "text/csv"})
public List<UserDto> exportUsers() {
    return userService.findAll();
}

Content-Type Header — İstek Formatı

Content negotiation sadece yanıt formatını değil, istek formatını da kapsar. Content-Type header'ı istemcinin gönderdiği verinin formatını belirtir:

# JSON gönderiyorum
POST /api/users HTTP/1.1
Content-Type: application/json

{"name": "Ahmet", "email": "ahmet@mail.com"}

# Form verisi gönderiyorum
POST /api/users HTTP/1.1
Content-Type: application/x-www-form-urlencoded

name=Ahmet&email=ahmet@mail.com

# XML gönderiyorum
POST /api/users HTTP/1.1
Content-Type: application/xml

<user><name>Ahmet</name><email>ahmet@mail.com</email></user>

Spring MVC, Content-Type header'ına bakarak uygun HttpMessageConverter'ı seçer ve istek gövdesini Java nesnesine dönüştürür.

Negotiation Stratejileri Karşılaştırma

StratejiAvantajDezavantajKullanım
Accept HeaderHTTP standartlarına uygunTarayıcıda test zorÖnerilen — REST standartı
Query Parameter (?format=)Kolay test, bookmarkableURL kirliliğiTarayıcı test, basit API'ler
URL Suffix (.json, .xml)Kolay anlaşılırSpring 5.3+ deprecatedKullanmayın
Custom HeaderEsnekStandart dışıÖzel gereksinimler
URL Path (/api/v1/)Açık, basitFarklı controllerAPI versiyonlama

💡 İpucu: Modern REST API'lerde Accept header yaklaşımı standarttır. Query parameter ek bir kolaylık olarak eklenebilir ama birincil strateji olmamalıdır.

Gerçek Dünya Örneği: Multi-Format Export API

Bir raporlama API'sinde aynı verinin JSON, XML ve CSV formatlarında sunulması:

@RestController
@RequestMapping("/api/reports")
public class ReportController {

    private final ReportService reportService;

    public ReportController(ReportService reportService) {
        this.reportService = reportService;
    }

    // JSON — varsayılan, web ve mobil istemciler için
    @GetMapping(value = "/sales",
        produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<SalesReport> getSalesReportJson(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
        SalesReport report = reportService.generateSalesReport(from, to);
        return ResponseEntity.ok()
            .header("X-Report-Generated-At", Instant.now().toString())
            .body(report);
    }

    // XML — enterprise entegrasyonlar için (SOAP tabanlı sistemler)
    @GetMapping(value = "/sales",
        produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity<SalesReport> getSalesReportXml(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
        SalesReport report = reportService.generateSalesReport(from, to);
        return ResponseEntity.ok(report);
    }

    // CSV — Excel'de açmak için, finans departmanı kullanır
    @GetMapping(value = "/sales/csv",
        produces = "text/csv")
    public ResponseEntity<StreamingResponseBody> getSalesReportCsv(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) {
        
        StreamingResponseBody stream = out -> {
            Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
            writer.write('\uFEFF'); // BOM — Excel Türkçe karakter desteği
            writer.write("Tarih,Ürün,Miktar,Tutar\n");
            
            List<SalesItem> items = reportService.getSalesItems(from, to);
            for (SalesItem item : items) {
                writer.write(String.format("%s,%s,%d,%.2f\n",
                    item.getDate(), item.getProduct(),
                    item.getQuantity(), item.getAmount()));
            }
            writer.flush();
        };

        String filename = "sales_" + from + "_" + to + ".csv";
        return ResponseEntity.ok()
            .header("Content-Disposition", "attachment; filename=" + filename)
            .body(stream);
    }
}

Bu örnekte aynı satış raporu verisi üç farklı formatta sunuluyor:

  • JSON: React/Angular frontend veya mobil uygulama için

  • XML: Enterprise entegrasyonlar, SOAP tabanlı legacy sistemler için

  • CSV: Finans departmanının Excel'de açıp analiz yapması için

İstemci Accept header'ı ile istediği formatı seçer. Tek API, çoklu format — content negotiation'ın gücü budur.

Yaygın Hatalar

Hata 1: XML dependency'si eklenmeden XML produces tanımlamak

// ❌ 406 Not Acceptable alırsınız — XML converter yok!
@GetMapping(produces = MediaType.APPLICATION_XML_VALUE)
public User getUser() { return userService.find(); }

// Çözüm: jackson-dataformat-xml dependency ekleyin

Hata 2: Accept header'ı yanlış yorumlamak

// Accept: */* geldiğinde ilk uygun format döner (genellikle JSON)
// İstemci XML bekleyip JSON alabilir — açıkça belirtin!

Özet

  • Content negotiation, aynı endpoint'ten farklı formatlarda (JSON, XML, CSV) yanıt almayı sağlar. İstemci Accept header'ı ile format tercihini bildirir.

  • Accept header ile negotiation en standart yöntemdir. q parametresi ile tercih sırası belirtilir.

  • `produces/consumes` ile endpoint bazında format kısıtlaması yapın. API sözleşmenizi net tutun.

  • JSON varsayılan formattır, XML ek dependency (jackson-dataformat-xml) ile desteklenir.

  • Custom media type'lar ile API versiyonlama yapabilirsiniz: application/vnd.myapp.v2+json.

  • HttpMessageConverter zinciri, content negotiation'ın arka plandaki motordur. Custom converter yazarak özel formatlar (CSV, Protocol Buffers) desteklenebilir.