Configure FAIL_ON_UNKNOWN_PROPERTIES for each RequestMapping differently in the Controller

陌路散爱 提交于 2020-12-06 12:16:15

问题


I want to handle json to Object conversion differently on different @RequestMapping in my Controller.

I believe if we add Jackson dependency in our spring-boot project it handles json to Object conversion and #spring.jackson.deserialization.fail-on-unknown-properties=true property will make sure that conversion will fail if there is some unknown property present in the json (please correct me if I am wrong).

Can we tell jackson locally when to fail on unknown properties and when to ignore those property.

Following is code snippet to use a flag.

    @GetMapping(value = "sample")
    public @ResponseBody UserDTO test(@RequestParam String str, @RequestParam boolean failFast) {
        ObjectMapper map = new ObjectMapper();
        if( failFast) {
            map.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
        } else {
            map.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        }
        UserDTO userDTO = null;
        try {
            userDTO = map.readValue(str, UserDTO.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return userDTO;
    }

I don't need it to be handled at runtime like i am doing using @RequestParam. Is there some property using which i can use to mark mappings where to check for unknown properties and where to ignore them.

Edit: What I am looking for is to change an existing application to handle Unknown property per mapping. For example:

        @PostMapping(value = "fail/fast")
        public @ResponseBody UserDTO test(@FAIL_ON_UNKNOWN @RequestBody UserDTO userDTO, @RequestParam boolean failFast) {
            ..///processing...
            return userDTO;
        }

        @PostMapping(value = "fail/safe")
        public @ResponseBody UserDTO test( @RequestBody UserDTO userDTO, @RequestParam boolean failFast) {
                ..///processing...
                return userDTO;
        }

If some king of validation can be added per mapping then i don't need to change all existing mapping's to customise unknown property and code change will be minimum.


回答1:


Jackson's ObjectMapper allows you to create new ObjectReader with custom configuration. You can create one common ObjectMapper instance in your app and for some controllers use it as a base object for creating custom readers. It will allow you to use all common features and registered modules and change few if needed. See below controller:

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.util.Objects;

@RestController
@RequestMapping(value = "/options")
public class JacksonOptionsController {

    private final ObjectMapper objectMapper;

    @Autowired
    public JacksonOptionsController(ObjectMapper objectMapper) {
        this.objectMapper = Objects.requireNonNull(objectMapper);
    }

    @PostMapping(path = "/fail")
    public ResponseEntity<String> readAndFastFail(HttpServletRequest request) throws IOException {
        String json = readAsRawJSON(request);
        Payload payload = createFailFastReader().readValue(json);

        return ResponseEntity.ok("SUCCESS");
    }

    @PostMapping(path = "/success")
    public ResponseEntity<String> readAndIgnore(HttpServletRequest request) throws IOException {
        String json = readAsRawJSON(request);
        Payload payload = createSafeReader().readValue(json);

        return ResponseEntity.ok("SUCCESS");
    }

    private ObjectReader createFailFastReader() {
        return objectMapper
                .readerFor(Payload.class)
                .with(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }

    private ObjectReader createSafeReader() {
        return objectMapper
                .readerFor(Payload.class);
    }

    private String readAsRawJSON(HttpServletRequest request) throws IOException {
        try (InputStreamReader reader = new InputStreamReader(request.getInputStream())) {
            try (StringWriter out = new StringWriter(64)) {
                reader.transferTo(out);
                return out.toString();
            }
        }
    }
}

Payload class has only one property - id. In one controller we use ObjectReader with enabled DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES. In other we use ObjectReader with default configuration with disabled DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.

For a test request:

curl -i -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{"id":"some-value","id1":1}' http://localhost:8080/options/fail

app throws exception and for request:

curl -i -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{"id":"some-value"}' http://localhost:8080/options/fail

it returns SUCCESS value. When we send two above payloads on http://localhost:8080/options/success URL, app in both cases returns SUCCESS value.

See also:

  • How to access plain json body in Spring rest controller?
  • Spring Boot: Customize the Jackson ObjectMapper



回答2:


I was able to achieve the desired result by implementing my own HttpMessageConverter. Thanks to @MichalZiober for suggesting it.

I created a Custom HttpMessageConvertor and registered it with my custom MediaType:{"application", "json-failFast"}.

How this works is whenever Header: Content-Type:application/json-failFast is present then unknown properties in @RequestBody/@ResponseBody will not be accepted while converting from json to Object and UnrecognizedPropertyException will be thrown.

And whenever Header: Content-Type:application/json is present then unrecognised properties in @RequestBody/ResponseBody will be ignored.

Here is my custom HttpMessageConverter:

@Component
public class CustomJsonMessageConverter extends AbstractJackson2HttpMessageConverter {

    @Nullable
    private String jsonPrefix;

    public CustomJsonMessageConverter() {
        this(Jackson2ObjectMapperBuilder.json().build().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,true));
    }
    public CustomJsonMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, new MediaType[]{ new MediaType("application", "json-failFast")});
    }

    public void setJsonPrefix(String jsonPrefix) {
        this.jsonPrefix = jsonPrefix;
    }

    public void setPrefixJson(boolean prefixJson) {
        this.jsonPrefix = prefixJson ? ")]}', " : null;
    }

    protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
            if (this.jsonPrefix != null) {
            generator.writeRaw(this.jsonPrefix);
        }
    }
}



回答3:


@Autowired
private RequestMappingHandlerAdapter converter;

@Override
public void afterPropertiesSet() throws Exception {
    configureJacksonToFailOnUnknownProperties();
}

private void configureJacksonToFailOnUnknownProperties() {
    MappingJackson2HttpMessageConverter httpMessageConverter = converter.getMessageConverters().stream()
            .filter(mc -> mc.getClass().equals(MappingJackson2HttpMessageConverter.class))
            .map(mc -> (MappingJackson2HttpMessageConverter)mc)
            .findFirst()
            .get();

    httpMessageConverter.getObjectMapper().enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}


来源:https://stackoverflow.com/questions/58291233/configure-fail-on-unknown-properties-for-each-requestmapping-differently-in-the

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!