← Kursa Dön
📄 Text · 20 min

@RequestBody, JSON ve Jackson

Modern REST API'lerin temel veri formatı JSON (JavaScript Object Notation) dir. Spring Boot, JSON işleme için Jackson kütüphanesini kullanır ve bu kütüphane spring-boot-starter-web dependency'si ile otomatik olarak gelir. @RequestBody annotation'ı, HTTP istek gövdesindeki JSON verisini Java nesnesine dönüştürür (deserializasyon). Tersi işlem — Java nesnesini JSON'a çevirme — ise @ResponseBody (veya @RestController) ile otomatik olarak gerçekleşir (serializasyon).

Bu süreç bir çevirmenin işine benzer. İngilizce (JSON) bir mektup geliyor, çevirmen (Jackson) bunu Türkçe'ye (Java nesnesi) çeviriyor. Cevap yazılacağında Türkçe metin tekrar İngilizce'ye çevriliyor. Tüm bu çeviri işlemi arka planda, otomatik olarak gerçekleşir.

@RequestBody — JSON'dan Java Nesnesine

@RequestBody, HTTP istek gövdesindeki veriyi belirtilen Java nesnesine otomatik olarak dönüştürür. Bu, REST API'lerin temel taşıdır:

// DTO (Data Transfer Object) sınıfı
public class CreateUserRequest {
    private String name;
    private String email;
    private int age;

    // Getter ve Setter'lar (Jackson için gerekli)
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

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

    @PostMapping
    public User createUser(@RequestBody CreateUserRequest request) {
        // JSON otomatik olarak CreateUserRequest nesnesine dönüştürüldü
        System.out.println(request.getName());  // "Ahmet"
        System.out.println(request.getEmail()); // "ahmet@mail.com"
        return userService.create(request);
    }
}

Bu endpoint'e şu HTTP isteği gönderildiğinde:

POST /api/users
Content-Type: application/json

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

Jackson, JSON'daki alanları Java nesnesinin setter'ları veya field'ları ile eşleştirir ve nesneyi oluşturur. Bu eşleştirme alan adı bazlıdır — JSON'daki "name" Java'daki name field'ı ile eşleşir.

@RequestBody ile @Valid Birlikte Kullanımı

Gerçek uygulamalarda gelen veriyi doğrulamak kritiktir. Bean Validation ile birlikte kullanıldığında:

public class CreateUserRequest {

    @NotBlank(message = "İsim boş olamaz")
    @Size(min = 2, max = 100, message = "İsim 2-100 karakter olmalı")
    private String name;

    @NotBlank(message = "Email boş olamaz")
    @Email(message = "Geçerli bir email adresi giriniz")
    private String email;

    @Min(value = 18, message = "Yaş en az 18 olmalı")
    @Max(value = 120, message = "Yaş en fazla 120 olmalı")
    private int age;

    @NotBlank(message = "Şifre boş olamaz")
    @Size(min = 8, message = "Şifre en az 8 karakter olmalı")
    private String password;

    // Getter/Setter'lar...
}

@PostMapping
public ResponseEntity<User> createUser(@RequestBody @Valid CreateUserRequest request) {
    // @Valid sayesinde DTO otomatik doğrulanır
    // Validation hatası varsa MethodArgumentNotValidException fırlatılır
    User created = userService.create(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(created);
}

⚠️ Dikkat: @Valid eklemeyi unutursanız, gelen veri hiç doğrulanmaz. @Valid annotation'ı Spring'e "bu nesneyi doğrula" der. Validation hataları @ControllerAdvice ile global olarak yakalanabilir.

Jackson ObjectMapper — JSON Dönüştürme Motoru

Jackson'ın merkezinde ObjectMapper sınıfı bulunur. Tüm serializasyon (Java → JSON) ve deserializasyon (JSON → Java) işlemlerini bu sınıf yapar:

// ObjectMapper doğrudan kullanımı (test ve utility amaçlı)
ObjectMapper mapper = new ObjectMapper();

// Java → JSON (Serializasyon)
User user = new User("Ahmet", "ahmet@mail.com", 25);
String json = mapper.writeValueAsString(user);
// {"name":"Ahmet","email":"ahmet@mail.com","age":25}

// JSON → Java (Deserializasyon)
String jsonInput = "{\"name\":\"Ayşe\",\"email\":\"ayse@mail.com\",\"age\":30}";
User parsedUser = mapper.readValue(jsonInput, User.class);

// JSON → List (Generic Type)
String jsonArray = "[{\"name\":\"Ali\"},{\"name\":\"Veli\"}]";
List<User> users = mapper.readValue(jsonArray,
    new TypeReference<List<User>>() {});

// JSON → Map
Map<String, Object> map = mapper.readValue(jsonInput,
    new TypeReference<Map<String, Object>>() {});

// Pretty print
String prettyJson = mapper.writerWithDefaultPrettyPrinter()
    .writeValueAsString(user);

Spring Boot'ta ObjectMapper'ı doğrudan kullanmanıza genellikle gerek yoktur — @RequestBody ve @RestController arka planda bunu otomatik olarak yapar. Ancak test yazarken veya özel dönüşümler gerektiğinde doğrudan kullanışlıdır.

ObjectMapper Yapılandırması

Spring Boot'ta Jackson'ı application.properties ile yapılandırabilirsiniz:

# Tarih formatı — timestamp yerine ISO 8601
spring.jackson.serialization.write-dates-as-timestamps=false

# Bilinmeyen alanları yoksay (varsayılan: hata fırlatır)
spring.jackson.deserialization.fail-on-unknown-properties=false

# Null alanları dahil etme
spring.jackson.default-property-inclusion=non_null

# Pretty print (geliştirme ortamı için)
spring.jackson.serialization.indent-output=true

# Naming strategy — camelCase → snake_case
spring.jackson.property-naming-strategy=SNAKE_CASE

Veya Java config ile daha detaylı kontrol:

@Configuration
public class JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
        return builder -> builder
            .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
            .featuresToDisable(
                SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
                SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .featuresToEnable(
                DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .serializationInclusion(JsonInclude.Include.NON_NULL)
            .timeZone(TimeZone.getTimeZone("Europe/Istanbul"));
    }
}

Jackson Annotation'ları — Detaylı Kılavuz

Jackson, JSON serializasyon/deserializasyon sürecini kontrol etmek için zengin bir annotation seti sunar. Her birini detaylıca görelim.

@JsonProperty — Alan Adı Eşleştirme

JSON'daki alan adı ile Java'daki field adı farklı olduğunda:

public class UserDto {

    @JsonProperty("user_name")  // JSON: "user_name", Java: "userName"
    private String userName;

    @JsonProperty("email_address")  // JSON: "email_address", Java: "emailAddress"
    private String emailAddress;

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private Long id;  // Sadece serializasyonda (JSON'a yazarken) kullanılır
    // Deserializasyonda (JSON'dan okurken) yoksayılır

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;  // Sadece deserializasyonda kullanılır
    // Serializasyonda (JSON'a yazarken) dahil edilmez — güvenlik!
}

READ_ONLY ve WRITE_ONLY access modları çok güçlüdür: API yanıtında id gösterilir ama istemci onu set edemez. password istemci tarafından gönderilir ama API yanıtında asla görünmez.

@JsonIgnore ve @JsonIgnoreProperties — Alanı Gizleme

public class User {
    private Long id;
    private String name;

    @JsonIgnore  // Bu alan ne serializasyonda ne de deserializasyonda yer alır
    private String password;

    @JsonIgnore
    private String internalToken;
}
// JSON çıktısı: {"id": 1, "name": "Ahmet"}
// password ve internalToken dahil edilmez

// Sınıf seviyesinde birden fazla alanı gizleme
@JsonIgnoreProperties({"password", "token", "internalNotes"})
public class User {
    private Long id;
    private String name;
    private String password;    // Gizli
    private String token;        // Gizli
    private String internalNotes; // Gizli
}

// Bilinmeyen alanları yoksay (deserializasyonda hata fırlatma)
@JsonIgnoreProperties(ignoreUnknown = true)
public class ExternalApiResponse {
    private String result;
    private int statusCode;
    // JSON'da ek alanlar olsa bile hata vermez
    // Dış API'lerden gelen yanıtlarda çok kullanışlı
}

@JsonFormat — Tarih ve Sayı Formatı

public class Event {

    @JsonFormat(shape = JsonFormat.Shape.STRING,
        pattern = "dd-MM-yyyy HH:mm:ss",
        timezone = "Europe/Istanbul")
    private LocalDateTime startDate;

    @JsonFormat(shape = JsonFormat.Shape.STRING,
        pattern = "yyyy-MM-dd")
    private LocalDate eventDate;

    @JsonFormat(shape = JsonFormat.Shape.STRING)  // Sayıyı string olarak serialize et
    private BigDecimal price;
    
    @JsonFormat(pattern = "HH:mm")
    private LocalTime eventTime;
}
// JSON: {
//   "startDate": "15-03-2024 14:30:00",
//   "eventDate": "2024-03-15",
//   "price": "99.99",
//   "eventTime": "14:30"
// }

@JsonInclude — Null/Empty Alanları Hariç Tutma

@JsonInclude(JsonInclude.Include.NON_NULL)  // null alanları dahil etme
public class UserResponse {
    private Long id;
    private String name;
    private String middleName;  // null ise JSON'da yer almaz
    private List<String> tags;  // null ise JSON'da yer almaz
}
// name = "Ahmet", middleName = null, tags = null
// JSON: {"id": 1, "name": "Ahmet"} ← middleName ve tags yok

// Diğer seçenekler:
@JsonInclude(JsonInclude.Include.NON_EMPTY)   // Boş collection/string dahil etme
@JsonInclude(JsonInclude.Include.NON_DEFAULT) // Varsayılan değerleri dahil etme
@JsonInclude(JsonInclude.Include.ALWAYS)      // Her zaman dahil et (varsayılan)

@JsonNaming — Naming Strategy

// Sınıf seviyesinde naming strategy
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserDto {
    private String firstName;    // JSON: "first_name"
    private String lastName;     // JSON: "last_name"
    private String emailAddress; // JSON: "email_address"
    private int loginCount;      // JSON: "login_count"
}

@JsonCreator ve @JsonValue — İmmutable Nesneler

Immutable sınıflar (setter'sız, final field'lı) için constructor bazlı deserializasyon:

public class Money {
    private final BigDecimal amount;
    private final String currency;

    @JsonCreator
    public Money(
        @JsonProperty("amount") BigDecimal amount,
        @JsonProperty("currency") String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public BigDecimal getAmount() { return amount; }
    public String getCurrency() { return currency; }
}

// Enum serializasyonu
public enum OrderStatus {
    PENDING("Beklemede"),
    SHIPPED("Kargoda"),
    DELIVERED("Teslim Edildi"),
    CANCELLED("İptal");

    private final String displayName;

    OrderStatus(String displayName) { this.displayName = displayName; }

    @JsonValue  // Serializasyonda bu değer kullanılır
    public String getDisplayName() { return displayName; }

    @JsonCreator  // Deserializasyonda bu metot kullanılır
    public static OrderStatus fromDisplayName(String name) {
        return Arrays.stream(values())
            .filter(s -> s.displayName.equals(name))
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("Geçersiz status: " + name));
    }
}

Custom Serializer ve Deserializer

Standart Jackson davranışı yetersiz kaldığında özel serializasyon/deserializasyon yazabilirsiniz:

// Custom Serializer — Java → JSON dönüşümünü özelleştir
public class MoneySerializer extends JsonSerializer<BigDecimal> {
    @Override
    public void serialize(BigDecimal value, JsonGenerator gen,
                          SerializerProvider provider) throws IOException {
        gen.writeString(value.setScale(2, RoundingMode.HALF_UP) + " TL");
    }
}

// Custom Deserializer — JSON → Java dönüşümünü özelleştir
public class MoneyDeserializer extends JsonDeserializer<BigDecimal> {
    @Override
    public BigDecimal deserialize(JsonParser parser,
                                   DeserializationContext ctx) throws IOException {
        String value = parser.getValueAsString()
            .replace(" TL", "")
            .replace(".", "")
            .replace(",", ".");
        return new BigDecimal(value);
    }
}

// Kullanım
public class Product {
    private String name;

    @JsonSerialize(using = MoneySerializer.class)
    @JsonDeserialize(using = MoneyDeserializer.class)
    private BigDecimal price;
}
// JSON: {"name": "Laptop", "price": "15999.99 TL"}

Daha karmaşık bir örnek — maskelenmiş alan:

// Hassas verileri maskeleyerek serialize et
public class MaskedStringSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen,
                          SerializerProvider provider) throws IOException {
        if (value == null || value.length() < 4) {
            gen.writeString("****");
        } else {
            String masked = value.substring(0, 2) 
                + "*".repeat(value.length() - 4) 
                + value.substring(value.length() - 2);
            gen.writeString(masked);
        }
    }
}

public class UserResponse {
    private String name;
    private String email;

    @JsonSerialize(using = MaskedStringSerializer.class)
    private String phoneNumber;  // "05551234567" → "05*******67"

    @JsonSerialize(using = MaskedStringSerializer.class)
    private String tcKimlik;     // "12345678901" → "12*******01"
}

snake_case vs camelCase Konfigürasyonu

Java'da convention camelCase (firstName), birçok API'de ise convention snake_case (first_name) dir. Bu dönüşümü global olarak yapılandırabilirsiniz:

# application.properties — Global snake_case
spring.jackson.property-naming-strategy=SNAKE_CASE
// Veya sınıf bazında
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserDto {
    private String firstName;    // JSON: "first_name"
    private String lastName;     // JSON: "last_name"
    private String emailAddress; // JSON: "email_address"
}

Yaygın Hatalar ve Çözümleri

Hata 1: No default constructor found

Jackson, deserializasyon için parametresiz constructor (no-arg constructor) gerektirir:

// ❌ YANLIŞ — Parametresiz constructor yok
public class User {
    private String name;
    public User(String name) { this.name = name; }
}

// ✅ DOĞRU — Parametresiz constructor ekleyin
public class User {
    private String name;
    public User() {}  // Jackson için gerekli
    public User(String name) { this.name = name; }
}

// ✅ DOĞRU — Lombok ile
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private String name;
}

// ✅ DOĞRU — @JsonCreator ile immutable
public class User {
    private final String name;
    
    @JsonCreator
    public User(@JsonProperty("name") String name) {
        this.name = name;
    }
}

Hata 2: Unrecognized field

JSON'da sınıfta olmayan bir alan varsa varsayılan davranış hata fırlatır:

# Global ayar
spring.jackson.deserialization.fail-on-unknown-properties=false
// Veya sınıf bazında
@JsonIgnoreProperties(ignoreUnknown = true)
public class ExternalApiResponse { ... }

Hata 3: Infinite recursion — Çift yönlü ilişkiler

JPA entity'lerde çift yönlü ilişkilerde sonsuz döngü oluşabilir:

// ❌ YANLIŞ — Sonsuz döngü: User → orders → User → orders → ...
@Entity
public class User {
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}

@Entity
public class Order {
    @ManyToOne
    private User user;
}

// ✅ DOĞRU — @JsonManagedReference ve @JsonBackReference
@Entity
public class User {
    @OneToMany(mappedBy = "user")
    @JsonManagedReference  // Serializasyona dahil edilir (parent taraf)
    private List<Order> orders;
}

@Entity
public class Order {
    @ManyToOne
    @JsonBackReference  // Serializasyona dahil EDİLMEZ (child taraf)
    private User user;
}

// ✅ DAHA İYİ — DTO kullanın, entity'leri doğrudan döndürmeyin
public class UserDto {
    private Long id;
    private String name;
    private List<OrderSummaryDto> orders; // Basitleştirilmiş DTO
}

⚠️ Dikkat: Entity'leri doğrudan API yanıtı olarak döndürmek birçok soruna yol açar: sonsuz döngü, gereksiz veri sızıntısı, lazy-loading exception'ları. Her zaman DTO kullanın.

Hata 4: LocalDateTime serializasyon sorunu

// Jackson'da Java 8 Date/Time desteği modül gerektirir
// Spring Boot bunu otomatik yapılandırır, ama ayrı kullanıyorsanız:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Jackson Modülleri

Jackson, ek modüllerle genişletilebilir. Spring Boot'ta en yaygın kullanılanlar:

<!-- Java 8 Date/Time desteği (Spring Boot ile otomatik gelir) -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

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

<!-- Kotlin desteği -->
<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
</dependency>

<!-- Guava collection desteği -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-guava</artifactId>
</dependency>

Gerçek Dünya Örneği: Tam CRUD API

Tüm Jackson konseptlerini birleştiren gerçek bir örnek:

// Request DTO'ları
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class CreateProductRequest {
    @NotBlank private String productName;
    @NotBlank private String description;
    @Positive private BigDecimal unitPrice;
    @NotBlank private String categoryCode;
    
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String internalSku;
}

// Response DTO
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProductResponse {
    private Long id;
    private String productName;
    private String description;
    
    @JsonSerialize(using = MoneySerializer.class)
    private BigDecimal unitPrice;
    
    private String categoryCode;
    
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime createdAt;
    
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime updatedAt;  // null ise JSON'da yer almaz (NON_NULL)
}

// Controller
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE,
                 produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ProductResponse> create(
            @RequestBody @Valid CreateProductRequest request) {
        ProductResponse created = productService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
}

Özet

  • `@RequestBody` ile JSON → Java dönüşümü, @RestController ile Java → JSON dönüşümü otomatiktir. Jackson bu işlemi arka planda yapar.

  • Jackson annotation'ları ile serializasyonu özelleştirin: @JsonProperty ile alan adı eşleştirme, @JsonIgnore ile alanları gizleme, @JsonFormat ile tarih/sayı formatı.

  • Production'da `@JsonIgnore` ile hassas verileri koruyun. Şifre, token, dahili ID gibi alanlar API yanıtında asla görünmemeli.

  • Global naming strategy ile tutarlı API convention'ları sağlayın. snake_case veya camelCase seçin ve projede tutarlı kalın.

  • DTO kullanın — entity'leri doğrudan döndürmek sonsuz döngü, veri sızıntısı ve lazy-loading sorunlarına yol açar.

  • `@Valid` ile gelen veriyi doğrulayın. Doğrulama olmadan güvensiz veri iş mantığına ulaşır.