Java 中的 JSON 补丁请求验证

问题描述 投票:0回答:4

在我的 Spring Boot 服务中,我使用 https://github.com/java-json-tools/json-patch 来处理 PATCH 请求。

一切似乎都很好,除了一种避免修改对象 id、creation_time 等不可变字段的方法。我在 Github 上发现了类似的 question https://github.com/java-json-tools/json-patch/ issues/21 我找不到合适的例子。

这个博客似乎提供了一些关于使用node.js中的解决方案验证JSON补丁请求的有趣解决方案。很高兴知道 JAVA 中是否已经有类似的东西。

json rest spring-restcontroller spring-rest json-patch
4个回答
10
投票

在许多情况下,您可以只修补一个只有用户可以写入的字段的中间对象。之后,您可以使用某些对象映射器或手动将中间对象轻松映射到您的实体。

这样做的缺点是,如果您要求字段必须显式可为空,您将不知道补丁对象是否显式将字段设置为空,或者该字段是否从未出现在补丁中。

你也可以做的就是滥用选项,例如

public class ProjectPatchDTO {

    private Optional<@NotBlank String> name;
    private Optional<String> description;
}

虽然Options并不是为了这样使用,但它是在维护类型化输入的同时实现补丁操作的最直接的方法。当可选字段为空时,它永远不会从客户端传递。当可选值不存在时,这意味着客户端已将值设置为 null。


1
投票

不要直接从客户端接收

JsonPatch
,而是定义一个 DTO 来处理验证,然后您稍后会将 DTO 实例转换为
JsonPatch

假设您要更新实例

User.class
的用户,您可以定义一个DTO,例如:

public class UserDTO {

    @Email(message = "The provided email is invalid")
    private String username;

    @Size(min = 2, max = 10, message = "firstname should have at least 2 and a maximum of 10 characters")
    private String firstName;

    @Size(min = 2, max = 10, message = "firstname should have at least 2 and a maximum of 10 characters")
    private String lastName;

    @Override
    public String toString() {
        return new Gson().toJson(this);
    }

//getters and setters
}

自定义

toString
方法可确保未包含在更新请求中的字段不会预先填充
null
值。

您的

PATCH
请求可以如下(为简单起见,我没有考虑例外情况)

@PatchMapping("/{id}")
    ResponseEntity<Object> updateUser(@RequestBody @Valid UserDTO request,
                                      @PathVariable String id) throws ParseException, IOException, JsonPatchException {
        User oldUser = userRepository.findById(id);
        String detailsToUpdate = request.toString();
        User newUser = applyPatchToUser(detailsToUpdate, oldUser);
        userRepository.save(newUser);
        return userService.updateUser(request, id);
    }

以下方法返回上面在控制器中更新的修补用户。

private User applyPatchToUser(String detailsToUpdate, User oldUser) throws IOException, JsonPatchException {
        ObjectMapper objectMapper = new ObjectMapper();
        // Parse the patch to JsonNode
        JsonNode patchNode = objectMapper.readTree(detailsToUpdate);
        // Create the patch
        JsonMergePatch patch = JsonMergePatch.fromJson(patchNode);
        // Convert the original object to JsonNode
        JsonNode originalObjNode = objectMapper.valueToTree(oldUser);
        // Apply the patch
        TreeNode patchedObjNode = patch.apply(originalObjNode);
        // Convert the patched node to an updated obj
        return objectMapper.treeToValue(patchedObjNode, User.class);
    }

1
投票

另一个解决方案是强制反序列化并验证请求正文

因此您的示例 DTO 可能如下所示:

public class CatDto {
    @NotBlank
    private String name;

    @Min(0)
    @Max(100)
    private int laziness;

    @Max(3)
    private int purringVolume;
}

你的控制器可以是这样的:

@RestController
@RequestMapping("/api/cats")
@io.swagger.v3.oas.annotations.parameters.RequestBody(
        content = @Content(schema = @Schema(implementation = CatDto.class)))
// ^^ this passes your CatDto model to swagger (you must use springdoc to get it to work!)
public class CatController {
    @Autowired
    SmartValidator validator; // we'll use this to validate our request

    @PatchMapping(path = "/{id}", consumes = "application/json")
    public ResponseEntity<String> updateCat(
            @PathVariable String id,
            @RequestBody Map<String, Object> body
            // ^^ no Valid annotation, no declarative DTO binding here!
    ) throws MethodArgumentNotValidException {
        CatDto catDto = new CatDto();
        WebDataBinder binder = new WebDataBinder(catDto);
        BindingResult bindingResult = binder.getBindingResult();

        binder.bind(new MutablePropertyValues(body));
        // ^^ imperatively bind to DTO
        body.forEach((k, v) -> validator.validateValue(CatDto.class, k, v, bindingResult));
        // ^^ imperatively validate user input
        if (bindingResult.hasErrors()) {
            throw new MethodArgumentNotValidException(null, bindingResult);
            // ^^ this can be handled by your regular exception handler
        }
        // Here you can do normal stuff with your cat DTO.
        // Map it to cat model, send to cat service, whatever.
        return ResponseEntity.ok("cat updated");
    }

}

不需要可选的,没有额外的依赖,你的正常验证就可以了,你的招摇看起来不错。唯一的问题是,您没有在嵌套对象上获得正确的合并补丁,但在许多用例中,这甚至不是必需的。


0
投票

在 Spring 中(但不仅限于 Spring),您可以将 JSONObject 辅助类(由 spring 提供)与 ObjectMapper 一起使用,并使用 JSONObject 类中的 has 方法检查 patch 是否包含元素,如下所示:

var jsonObject = new JSONObject(objectMapper.writeValueAsString(patch));
if (jsonObject.has("property")) {
    // do logic here
}

在这种情况下,您可以对 json patch 或 merge patch 中的任何元素执行此操作,无论元素类型如何。这个解决方案不仅适用于 Spring。还有其他供应商提供 JSONObject 类来执行相同的操作(org.json 等)。并且使用不同的库也可以将 json patch 转换为字符串。

© www.soinside.com 2019 - 2024. All rights reserved.