Как можно различить ситуацию, когда поле отсутствует в теле запроса, и случай, когда это поле присутствует, но имеет значение null?

Я разрабатываю REST API с использованием Spring и столкнулся с проблемой в методе PATCH, который предназначен для частичного обновления сущности. В этом методе предполагается, что клиент будет отправлять только те поля, которые необходимо изменить. Учитывая, что у сущности есть nullable поля, возник вопрос: <b>как можно определить, установил ли клиент поле в значение null или не передал его вовсе</b>? В обоих случаях, при маппинге из запроса в объект с помощью @RequestBody, поле оказывается равным null.
  • 8 января 2025 г. 5:33
Ответы на вопрос 2
В REST API, особенно в контексте частичного обновления сущностей с использованием метода PATCH, действительно существует проблема различения случаев, когда поле отсутствует в теле запроса, и когда оно явно установлено в значение `null`. 

В Spring вы можете использовать несколько подходов для решения этой проблемы:

1. **Использование объеков-оберток:**
   Один из распространенных способов это создание класса-обертки для обновляемых полей. Вместо того чтобы использовать непосредственно свойство типа, вы можете использовать тип-обертку, такой как `Optional` или специальный класс, который будет содержать информацию о том, было ли поле установлено. Например:

   ```java
   public class PasswordUpdateRequest {
       private String password;

       public Optional<String> getPassword() {
           return Optional.ofNullable(password);
       }

       // setPassword и другие геттеры/сеттеры...
   }
   ```

   При этом, если клиент не передаст поле `password`, `getPassword()` вернёт `Optional.empty()`, а если передаст с `null`, то `getPassword()` вернёт `Optional.ofNullable(null)`. Затем вы можете обрабатывать эти два случая при обновлении сущности.

2. **Использование кастомного десериализатора:**
   Вы можете создать кастомный десerializer, который будет десериализовать тело запроса, отличая `null` от отсутствующего поля. Это может быть немного сложнее, но дает вам максимальную гибкость.

3. **Использование `Map` в качестве тела запроса:**
   Вместо использования фиксированного класса, вы можете использовать `Map<String, Object>` в качестве элемента `@RequestBody`. Это позволит вам явно видеть, какие поля были переданы и установить соответствующие значения.

   ```java
   @PatchMapping("/your-endpoint")
   public ResponseEntity<YourEntity> patchEntity(@RequestBody Map<String, Object> updates) {
       if (updates.containsKey("fieldName")) {
           if (updates.get("fieldName") == null) {
               // Поле было передано с значением null
           } else {
               // Поле было передано с конкретным значением
           }
       } else {
           // Поле не было передано
       }
   }
   ```

4. **Использование аннотации `JsonInclude`:**
   В случае, если вы используете библиотеку Jackson, можно настраивать поведение сериализации и десериализации с помощью аннотации `@JsonInclude`, чтобы настраивать, как поля обрабатываются в JSON.

Выбор наилучшего подхода зависит от контекста вашего приложения и требований к API. Используйте тот метод, который вам наиболее удобен и соответствует дизайну вашего API.
Вообще-то никак. 
Есть, к счастью, куча способов решить этот вопрос.
0. Не использовать странный метод PATCH. Просто обновлять всю модель целиком методом POST
1. Если по какой-то причинне хочется "частичное обновление" сделать - можно сделать, например, какой-то флаг, который бы указывал, будут ли обновляться нулабельные поля. Можно подумать так же об отдельном методе.
2. Для метода PATCH разработать новую модель, которая будет указывать на поля и их новые значение. Типа мапы.
Т.е. для record User(String name, String phoneNumber){}
Принимать что-то типа такого джейсона:
{
   toUpdate : {
      "name" : "newName"
   },
   toDelete:  {
     phoneNumber
   }
 }


Похоже, что этот вопрос возник из-за желания использовать доменную модель во внешней интеграции, что является ошибкой проектирования. Как только вы откажетесь от такого подхода и будете использовать для внешних интеграций отдельные, специализированные модельки - все встанет на свои места.
Похожие вопросы