This is an automated email from the ASF dual-hosted git repository.

pefernan pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/incubator-kie-kogito-runtimes.git


The following commit(s) were added to refs/heads/main by this push:
     new 901c5908b2 [incubator-kie-issues-2367] PATCH API ignores explicit JSON 
null values for String process variables (#4335)
901c5908b2 is described below

commit 901c5908b2ee7659033ff963bdd6b2cf8992cb5e
Author: Alisha Mohamed Ali <[email protected]>
AuthorDate: Fri Jul 17 18:25:52 2026 +0530

    [incubator-kie-issues-2367] PATCH API ignores explicit JSON null values for 
String process variables (#4335)
    
    * PATCH API ignores explicit JSON null values for String process variables
    
    * Make present fieds hidden
    
    * Preserve explicit null vs omitted field distinction on PATCH
    
    * fix comments
    
    * fix buid chain
    
    * fix buid chain
    
    * Update MapOutput.java
    
    * fix formating
    
    * Adding tests
    
    * add tests
    
    * fix formatimg error
    
    * Add tests to have both PATCH and PUT calls on a resource,
    
    ---------
    
    Co-authored-by: Alisha Mohamed Ali <[email protected]>
---
 .../src/main/java/org/kie/kogito/Model.java        |  13 +-
 .../src/main/java/org/kie/kogito/Models.java       |   8 +-
 .../org/jbpm/compiler/canonical/ModelMetaData.java |  25 +++-
 .../canonical/ProcessToExecModelGenerator.java     |   2 +
 .../class-templates/ModelNoIDTemplate.java         |  21 ++-
 .../resources/class-templates/ModelTemplate.java   |  35 ++++-
 .../RestResourceQuarkusTemplate.java               |   4 +-
 .../RestResourceSpringTemplate.java                |   4 +-
 .../integrationtests/quarkus/BasicRestIT.java      |  86 +++++++++++
 .../kie/kogito/integrationtests/quarkus/OASIT.java |  21 +++
 .../src/main/resources/AdHocFragments.bpmn         |   2 +
 .../integrationtests/springboot/BasicRestTest.java | 159 +++++++++++++++++++++
 12 files changed, 370 insertions(+), 10 deletions(-)

diff --git a/api/kogito-api/src/main/java/org/kie/kogito/Model.java 
b/api/kogito-api/src/main/java/org/kie/kogito/Model.java
index fbe9acb8de..a4583aa74f 100644
--- a/api/kogito-api/src/main/java/org/kie/kogito/Model.java
+++ b/api/kogito-api/src/main/java/org/kie/kogito/Model.java
@@ -19,8 +19,7 @@
 package org.kie.kogito;
 
 import java.util.Map;
-import java.util.Map.Entry;
-import java.util.stream.Collectors;
+import java.util.Set;
 
 /**
  * Represents data model type of objects that are usually descriptor of data 
holders.
@@ -33,8 +32,16 @@ public interface Model extends MapInput, MapOutput {
     }
 
     default Map<String, Object> updatePartially(Map<String, Object> params) {
-        params = params.entrySet().stream().filter(e -> e.getValue() != 
null).collect(Collectors.toMap(Entry::getKey, Entry::getValue));
         update(params);
         return params;
     }
+
+    /**
+     * Returns the set of field names that were explicitly set (e.g. during a 
PATCH request).
+     * Returns null when tracking is not active — all fields are included in 
toMap().
+     * Returns a non-null set when tracking is active — only those fields are 
included.
+     */
+    default Set<String> getModifiedFields() {
+        return null;
+    }
 }
diff --git a/api/kogito-api/src/main/java/org/kie/kogito/Models.java 
b/api/kogito-api/src/main/java/org/kie/kogito/Models.java
index c1e57052d6..c91ca522fe 100644
--- a/api/kogito-api/src/main/java/org/kie/kogito/Models.java
+++ b/api/kogito-api/src/main/java/org/kie/kogito/Models.java
@@ -21,6 +21,7 @@ package org.kie.kogito;
 import java.lang.reflect.Field;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Set;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 
@@ -30,12 +31,17 @@ public class Models {
     }
 
     @SuppressWarnings("squid:S3011")
-    public static Map<String, Object> toMap(Object m) {
+    public static Map<String, Object> toMap(MapOutput m) {
+        Set<String> modifiedFields = (m instanceof Model model) ? 
model.getModifiedFields() : null;
         Map<String, Object> map = new LinkedHashMap<>();
         for (Field field : m.getClass().getDeclaredFields()) {
             JsonProperty jsonAnnotation = 
field.getAnnotation(JsonProperty.class);
             if (jsonAnnotation != null) {
                 String name = jsonAnnotation.value();
+                // null = not tracking (include all); non-null = include only 
modified fields
+                if (modifiedFields != null && !modifiedFields.contains(name)) {
+                    continue;
+                }
                 field.setAccessible(true);
                 try {
                     map.put(name, field.get(m));
diff --git 
a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
 
b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
index 62f35f59cd..bd3d64d21a 100644
--- 
a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
+++ 
b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
@@ -190,6 +190,18 @@ public class ModelMetaData {
         }
         modelClass.setName(modelClassSimpleName);
 
+        // The modified-fields tracking field/getter must stay out of the 
generated OpenAPI
+        if (supportsOpenApiGeneration) {
+            NormalAnnotationExpr hiddenSchema = new NormalAnnotationExpr(new 
Name(Schema.class.getCanonicalName()),
+                    NodeList.nodeList(new MemberValuePair("hidden", new 
BooleanLiteralExpr(true))));
+            modelClass.findFirst(FieldDeclaration.class,
+                    fd -> fd.getVariables().stream().anyMatch(v -> 
v.getNameAsString().equals("__modifiedFields")))
+                    .ifPresent(fd -> fd.addAnnotation(hiddenSchema.clone()));
+            modelClass.findFirst(MethodDeclaration.class, md -> 
md.getNameAsString().equals("getModifiedFields"))
+                    .ifPresent(md -> md.addAnnotation(hiddenSchema.clone()));
+            compilationUnit.addImport(Schema.class.getCanonicalName());
+        }
+
         // setup of the toMap method body
         BlockStmt toMapBody = new BlockStmt();
         ClassOrInterfaceType toMap = new ClassOrInterfaceType(null, new 
SimpleName(Map.class.getSimpleName()),
@@ -225,7 +237,14 @@ public class ModelMetaData {
             applyOpenApiSchemaAnnotation(fd);
 
             fd.createGetter();
-            fd.createSetter();
+            MethodDeclaration setter = fd.createSetter();
+            if (isInputModel()) {
+                // Only Input setters run from request deserialization - 
Model/Output setters
+                // are also called by generated copy loops, which would 
wrongly count as "set".
+                setter.getBody().ifPresent(setterBody -> 
setterBody.addStatement(
+                        new MethodCallExpr(null, "markModified",
+                                NodeList.nodeList(new 
StringLiteralExpr(varName)))));
+            }
 
         }
 
@@ -265,6 +284,10 @@ public class ModelMetaData {
                 .addModifier(Modifier.Keyword.PRIVATE);
     }
 
+    private boolean isInputModel() {
+        return "/class-templates/ModelNoIDTemplate.java".equals(templateName);
+    }
+
     public String getModelClassSimpleName() {
         return modelClassSimpleName;
     }
diff --git 
a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
 
b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
index b78ed1741f..1692b928f5 100644
--- 
a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
+++ 
b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
@@ -222,6 +222,8 @@ public class ProcessToExecModelGenerator {
                         "get" +
                                 fieldName)));
             }
+            body.addStatement(new MethodCallExpr(returnName, 
"addModifiedFields",
+                    NodeList.nodeList(new MethodCallExpr(null, 
"getModifiedFields"))));
             body.addStatement(new ReturnStmt(returnName));
             method.setBody(body);
         }
diff --git 
a/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
 
b/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
index c5d6fdd049..f66bd652d5 100644
--- 
a/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
+++ 
b/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
@@ -24,11 +24,30 @@ import org.kie.kogito.MapOutput;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
 
 import org.kie.kogito.MappableToModel;
 import org.kie.kogito.Model;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 public class XXXModel implements Model, MapInput, MapInputId, MapOutput,
                                  MappableToModel<$modelClass$> {
 
-}
\ No newline at end of file
+    // Ephemeral REST request-body carrier - always tracks modifications from 
construction.
+    @JsonIgnore
+    private transient final Set<String> __modifiedFields = new HashSet<>();
+
+    @JsonIgnore
+    @Override
+    public Set<String> getModifiedFields() {
+        return __modifiedFields;
+    }
+
+    // Marks a field as explicitly set, so PATCH can tell a null apart from an 
omitted field.
+    void markModified(String field) {
+        __modifiedFields.add(field);
+    }
+
+}
diff --git 
a/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java 
b/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
index b33106e9e8..b74cb6e7b3 100644
--- 
a/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
+++ 
b/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
@@ -24,20 +24,51 @@ import org.kie.kogito.MapOutput;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
 
 import org.kie.kogito.MappableToModel;
 import org.kie.kogito.Model;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 public class XXXModel implements org.kie.kogito.Model, MapInput, MapInputId, 
MapOutput, MappableToModel<$modelClass$> {
 
     private String id;
 
+    @JsonIgnore
+    private transient Set<String> __modifiedFields;
+
     public void setId(String id) {
         this.id = id;
     }
-    
+
     public String getId() {
         return this.id;
     }
 
-}
\ No newline at end of file
+    @JsonIgnore
+    @Override
+    public Set<String> getModifiedFields() {
+        return __modifiedFields;
+    }
+
+    // Codegen-internal: carries the modified-fields set from a PATCH Input 
onto
+    // its toModel() target. Left null otherwise, so toMap() includes every 
field.
+    void addModifiedFields(Set<String> fields) {
+        if (fields == null) {
+            return;
+        }
+        if (__modifiedFields == null) {
+            __modifiedFields = new HashSet<>();
+        }
+        __modifiedFields.addAll(fields);
+    }
+
+    // Called by the generated PUT handler before a full-replace update, so 
every
+    // field counts as significant - unlike PATCH, which only touches tracked 
fields.
+    void clearModifiedFields() {
+        __modifiedFields = null;
+    }
+
+}
diff --git 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
index 6202d1032e..18d5e778b2 100644
--- 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
+++ 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
@@ -160,7 +160,9 @@ public class $Type$Resource {
     @Produces(MediaType.APPLICATION_JSON)
     @Operation(operationId = "updateProcessInstance_$name$", summary = 
"$documentation$", description = "$processInstanceDescription$")
     public $Type$Output updateModel_$name$(@PathParam("id") String id, 
$Type$Input resource) {
-        return processService.update(process, id, 
resource.toModel()).orElseThrow(NotFoundException::new);
+        $Type$ model = resource.toModel();
+        model.clearModifiedFields();
+        return processService.update(process, id, 
model).orElseThrow(NotFoundException::new);
     }
     
     @PATCH
diff --git 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
index fe227d7538..5328f80d17 100644
--- 
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
+++ 
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
@@ -112,7 +112,9 @@ public class $Type$Resource {
             consumes = MediaType.APPLICATION_JSON_VALUE)
     @Operation(operationId = "updateProcessInstance_$name$", summary = 
"$documentation$", description = "$processInstanceDescription$")
     public $Type$Output updateModel_$name$(@PathVariable("id") String id, 
@RequestBody(required = false) $Type$Input resource) {
-        return processService.update(process, id, 
resource.toModel()).orElseThrow(() -> new 
ResponseStatusException(HttpStatus.NOT_FOUND));
+        $Type$ model = resource.toModel();
+        model.clearModifiedFields();
+        return processService.update(process, id, model).orElseThrow(() -> new 
ResponseStatusException(HttpStatus.NOT_FOUND));
     }
     
     @PatchMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE,
diff --git 
a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
 
b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
index 48fc895af4..39f5268b84 100644
--- 
a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
+++ 
b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
@@ -252,6 +252,92 @@ class BasicRestIT {
         assertExpectedUnitOfWorkEvents(2);
     }
 
+    @Test
+    void testPatchWithNullValue() {
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(getParams())
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .body("var2", equalTo(34))
+                .extract()
+                .path("id");
+
+        // var1 set to null must be cleared, unlike an omitted field
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": null}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", nullValue())
+                .body("var2", is(34));
+
+        assertExpectedUnitOfWorkEvents(2);
+    }
+
+    @Test
+    void testPatchAndPutSequence() {
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(getParams())
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .body("var2", equalTo(34))
+                .extract()
+                .path("id");
+
+        // PATCH: var1 explicitly nulled, var2 untouched
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": null}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", nullValue())
+                .body("var2", is(34));
+
+        // PUT right after a PATCH on the same resource: var2 omitted -> must 
still be nulled out
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": \"Gonzo\"}")
+                .put("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        // PATCH right after a PUT: empty body must leave both fields untouched
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        assertExpectedUnitOfWorkEvents(4);
+    }
+
     @Test
     void testDelete() {
         Map<String, String> params = new HashMap<>();
diff --git 
a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
 
b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
index 50575452ae..9db03fe82f 100644
--- 
a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
+++ 
b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
@@ -27,6 +27,7 @@ import io.quarkus.test.junit.QuarkusIntegrationTest;
 import io.restassured.RestAssured;
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.media.Schema;
 import io.swagger.v3.parser.OpenAPIV3Parser;
 import io.swagger.v3.parser.core.models.ParseOptions;
 import io.swagger.v3.parser.core.models.SwaggerParseResult;
@@ -60,4 +61,24 @@ class OASIT {
         assertThat(p1.getPost()).isNotNull();
     }
 
+    @Test
+    public void testApprovalsSchemaProperties() {
+        String url = rootUrl.toString() + "/q/openapi";
+        ParseOptions parseOptions = new ParseOptions();
+        parseOptions.setResolve(true);
+        SwaggerParseResult result = new OpenAPIV3Parser().readLocation(url, 
null, parseOptions);
+
+        OpenAPI openAPI = result.getOpenAPI();
+        PathItem p1 = openAPI.getPaths().get("/approvals");
+        assertThat(p1).isNotNull();
+
+        Schema requestSchema = 
p1.getPost().getRequestBody().getContent().get("application/json").getSchema();
+        String schemaName = 
requestSchema.get$ref().substring(requestSchema.get$ref().lastIndexOf('/') + 1);
+        Schema resolvedSchema = 
openAPI.getComponents().getSchemas().get(schemaName);
+        assertThat(resolvedSchema).isNotNull();
+
+        assertThat(resolvedSchema.getProperties().keySet())
+                .containsExactlyInAnyOrder("approver", "traveller", 
"firstLineApproval", "secondLineApproval");
+    }
+
 }
diff --git 
a/springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
 
b/springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
index a9bdf9430c..02675c1d61 100644
--- 
a/springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
+++ 
b/springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
@@ -19,6 +19,7 @@
   -->
 <bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"; 
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"; 
xmlns:bpsim="http://www.bpsim.org/schemas/1.0"; 
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"; 
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"; 
xmlns:drools="http://www.jboss.org/drools"; id="_WL3jkCdwED2KYMa5towL6A" 
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd 
http://www. [...]
   <bpmn2:itemDefinition id="_var1Item" structureRef="String"/>
+  <bpmn2:itemDefinition id="_var2Item" structureRef="Integer"/>
   <bpmn2:itemDefinition 
id="__731518E6-88A2-4CCC-A2C0-97C45189A7F3_ConditionInputXItem" 
structureRef="String"/>
   <bpmn2:itemDefinition 
id="__17762652-67B8-46FF-ADFE-D7AF3EA2ADAB_SkippableInputXItem" 
structureRef="Object"/>
   <bpmn2:itemDefinition 
id="__17762652-67B8-46FF-ADFE-D7AF3EA2ADAB_PriorityInputXItem" 
structureRef="Object"/>
@@ -64,6 +65,7 @@
   </bpmn2:collaboration>
   <bpmn2:process id="TestCase.AdHocFragments" 
drools:packageName="org.kie.kogito" drools:version="1.0" drools:adHoc="true" 
name="AdHocFragments" isExecutable="true" processType="Public">
     <bpmn2:property id="var1" itemSubjectRef="_var1Item" name="var1"/>
+    <bpmn2:property id="var2" itemSubjectRef="_var2Item" name="var2"/>
     <bpmn2:sequenceFlow id="_830D53F8-A344-45AE-A1A1-2173DD983A43" 
sourceRef="_89FFDFF9-8D8A-4B86-B48C-76F30299D8D3" 
targetRef="_319B612D-CD70-4FB0-8390-2F825115847E">
       <bpmn2:extensionElements>
         <drools:metaData name="isAutoConnection.source">
diff --git 
a/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
 
b/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
index db13274bf8..c4655363d4 100644
--- 
a/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
+++ 
b/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
@@ -38,6 +38,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.Matchers.emptyOrNullString;
+import static org.hamcrest.Matchers.nullValue;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 
classes = KogitoSpringbootApplication.class)
@@ -207,6 +208,164 @@ class BasicRestTest extends BaseRestTest {
         assertExpectedUnitOfWorkEvents(2);
     }
 
+    @Test
+    void testUpdateNullsOmittedField() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("var1", "Kermit");
+        params.put("var2", 34);
+
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(params)
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .body("var2", equalTo(34))
+                .extract()
+                .path("id");
+
+        // var2 omitted from PUT body -> full replace must null it
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": \"Gonzo\"}")
+                .put("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        assertExpectedUnitOfWorkEvents(2);
+    }
+
+    @Test
+    void testPatch() {
+        Map<String, String> params = new HashMap<>();
+        params.put("var1", "Kermit");
+
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(params)
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .extract()
+                .path("id");
+
+        // An empty PATCH body must leave existing variables untouched.
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Kermit"));
+
+        assertExpectedUnitOfWorkEvents(2);
+    }
+
+    @Test
+    void testPatchWithNullValue() {
+        Map<String, String> params = new HashMap<>();
+        params.put("var1", "Kermit");
+
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(params)
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .extract()
+                .path("id");
+
+        // var1 set to null must be cleared, unlike an omitted field
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": null}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", nullValue());
+
+        assertExpectedUnitOfWorkEvents(2);
+    }
+
+    @Test
+    void testPatchAndPutSequence() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("var1", "Kermit");
+        params.put("var2", 34);
+
+        String id = given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body(params)
+                .post("/AdHocFragments")
+                .then()
+                .statusCode(201)
+                .header("Location", not(emptyOrNullString()))
+                .body("id", not(emptyOrNullString()))
+                .body("var1", equalTo("Kermit"))
+                .body("var2", equalTo(34))
+                .extract()
+                .path("id");
+
+        // PATCH: var1 explicitly nulled, var2 untouched
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": null}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", nullValue())
+                .body("var2", equalTo(34));
+
+        // PUT right after a PATCH on the same resource: var2 omitted -> must 
still be nulled out
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{\"var1\": \"Gonzo\"}")
+                .put("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        // PATCH right after a PUT: empty body must leave both fields untouched
+        given()
+                .contentType(ContentType.JSON)
+                .when()
+                .body("{}")
+                .patch("/AdHocFragments/{customId}", id)
+                .then()
+                .statusCode(200)
+                .body("id", equalTo(id))
+                .body("var1", equalTo("Gonzo"))
+                .body("var2", nullValue());
+
+        assertExpectedUnitOfWorkEvents(4);
+    }
+
     @Test
     void testDelete() {
         Map<String, String> params = new HashMap<>();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to