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:
İstemci →
Acceptheader ile hangi formatı istediğini bildirirSunucu → İsteği karşılayabilecek en uygun formatı seçer
Sunucu →
Content-Typeheader 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.5q 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 tercihSunucu, 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/xmlileconsumes = "application/json"endpoint'ine istek gönderirse → 415 Unsupported Media Typeİstemci
Accept: text/htmlileproduces = "application/json"endpoint'ine istek gönderirse → 406 Not Acceptable
⚠️ Dikkat:
producesveconsumesbelirtilmezse 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.+jsonsuffix'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:
İstemcinin
Acceptheader'ını parse etController'ın
producesdeğerini kontrol etKayıtlı converter'lar arasında uyumlu olanı bul
En yüksek q değerine sahip eşleşmeyi seç
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
| Strateji | Avantaj | Dezavantaj | Kullanım |
|---|---|---|---|
| Accept Header | HTTP standartlarına uygun | Tarayıcıda test zor | Önerilen — REST standartı |
Query Parameter (?format=) | Kolay test, bookmarkable | URL kirliliği | Tarayıcı test, basit API'ler |
URL Suffix (.json, .xml) | Kolay anlaşılır | Spring 5.3+ deprecated | Kullanmayın |
| Custom Header | Esnek | Standart dışı | Özel gereksinimler |
URL Path (/api/v1/) | Açık, basit | Farklı controller | API 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 ekleyinHata 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
Acceptheader'ı ile format tercihini bildirir.Accept header ile negotiation en standart yöntemdir.
qparametresi 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.
AI Asistan
Sorularını yanıtlamaya hazır