3

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.

Mohd Waseem
  • 1,016
  • 2
  • 12
  • 29
  • 1
    You can differentiate per DTO by specifying it at the class level what to do with properties. – M. Deinum Oct 08 '19 at 18:51
  • @Deinum but then it will be fixed for this class. Suppose in some cases it is ok to have unknown property in the DTO but for other cases it is not ok. how we will handle these cases if we specify at class level. – Mohd Waseem Oct 09 '19 at 06:27
  • Use different DTOs. So unless you want to manually create all the `ObjectMapper` instances yourself and do the marshaling yourself as well, your option is to use different DTOs. – M. Deinum Oct 09 '19 at 06:31

3 Answers3

2

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:

Michał Ziober
  • 34,365
  • 17
  • 89
  • 132
  • thanks for the answer. I want to do something similar but for my existing application. Here we are working on HttpRequest itself and converting on our own. But all my controllers mapping are receiving DTO after converted from json by jackson. I need to hint jackson before converting json to DTO , when to check for unKnown property. Is it possible at all to configure unknown property failure per mapping at the time of json to DTO conversion. – Mohd Waseem Oct 09 '19 at 06:14
  • 1
    @MohdWaseem, this new requirement changes perspective. You need to take a look at [MappingJackson2HttpMessageConverter](https://www.baeldung.com/spring-httpmessageconverter-rest) class. Maybe you could extend it and configure `ObjectReader` there. – Michał Ziober Oct 09 '19 at 07:47
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);
        }
    }
}
Mohd Waseem
  • 1,016
  • 2
  • 12
  • 29
0
@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);
}
dhruv_0707
  • 41
  • 7