Hibernate Validator and Jackson: Using the @JsonProperty value as the ConstraintViolation PropertyPath?
Asked Answered
B

3

7

Say I have a simple POJO like below annotated with Jackson 2.1 and Hibernate Validator 4.3.1 annotations:

final public class Person {
  @JsonProperty("nm")
  @NotNull
  final public String name;

  public Person(String name) {
      this.name = name;
  }
}

And I send JSON like such to a web service:

{"name": null}

Hibernate when it reports the ConstraintViolation uses the class member identifier "name" instead of the JsonProperty annotation value. Does anyone know if it is possible to make the Hibernate Validator look at the annotation of the class and use that value instead?

Boggs answered 18/9, 2013 at 17:43 Comment(0)
J
4

No, that's not possible. Hibernate Validator 5 (Bean Validation 1.1) has the notion of ParameterNameProviders which return the names to reported in case method parameter constraints are violated but there is nothing comparable for property constraints.

Jea answered 18/9, 2013 at 21:41 Comment(3)
I've filed issue HV-823 for supporting this use case in Hibernate Validator.Jea
Bummer! But also Thank you for opening a feature request.Boggs
This is now supported by Hibernate Validator. Documentation: docs.jboss.org/hibernate/stable/validator/reference/en-US/…Inkblot
M
7

Unfortunately there is no easy way to do it. But here are some insights that can help you:

Parsing constraint violations

From the ConstraintViolationException, you can get a set of ConstraintViolation, that exposes the constraint violation context:

From the property path, you can get the leaf node:

Path propertyPath = constraintViolation.getPropertyPath();
Optional<Path.Node> leafNodeOptional = 
        StreamSupport.stream(propertyPath.spliterator(), false).reduce((a, b) -> b);

Then check if the type of the node is PROPERTY and get its name:

String nodeName = null;

if (leafNodeOptional.isPresent()) {
    Path.Node leafNode = leafNodeOptional.get();
    if (ElementKind.PROPERTY == leafNode.getKind()) {
        nodeName = leafNode.getName();
    }
}

Introspecting a class with Jackson

To get the available JSON properties from the leaf bean class, you can introspect it with Jackson (see this answer and this answer for further details):

Class<?> beanClass = constraintViolation.getLeafBean().getClass();
JavaType javaType = mapper.getTypeFactory().constructType(beanClass);

BeanDescription introspection = mapper.getSerializationConfig().introspect(javaType);
List<BeanPropertyDefinition> properties = introspection.findProperties();

Then filter the properties by comparing the leaf node name with the Field name from the BeanPropertyDefinition:

Optional<String> jsonProperty = properties.stream()
        .filter(property -> nodeName.equals(property.getField().getName()))
        .map(BeanPropertyDefinition::getName)
        .findFirst();

Using JAX-RS?

With JAX-RS (if you are using it), you can define an ExceptionMapper to handle ConstraintViolationExceptions:

@Provider
public class ConstraintViolationExceptionMapper 
                 implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        ...
    }
}

To use the ObjectMapper in your ExceptionMapper, you could provide a ContextResolver<T> for it:

@Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {

    private final ObjectMapper mapper;

    public ObjectMapperContextResolver() {
        mapper = createObjectMapper();
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return mapper;
    }

    private ObjectMapper createObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        return mapper;
    }
}

Inject the Providers interface in your ExceptionMapper:

@Context
private Providers providers;

Lookup for your ContextResolver<T> and then get the ObjectMapper instance:

ContextResolver<ObjectMapper> resolver = 
        providers.getContextResolver(ObjectMapper.class, MediaType.WILDCARD_TYPE);
ObjectMapper mapper = resolver.getContext(ObjectMapper.class);

If you are interested in getting @XxxParam names, refer to this answer.

Metrics answered 31/5, 2017 at 10:7 Comment(2)
Interesting, unfortunately this answer does not cover the @xxxParam inside an object, e.g. class SearchParams { @QueryParam("from") @Min(1) Integer fromParam; @QueryParam("to") @Min(2) Integer toParam; } / @GET @Path("search") Response search(@Valid @BeanParam SearchParams params) { ...Stallings
@Brice That's true. It can be addressed without relying on a ParameterNameProvider. Check my ConstraintViolationExceptionMapper on GitHub. Improvements are welcome :)Metrics
J
4

No, that's not possible. Hibernate Validator 5 (Bean Validation 1.1) has the notion of ParameterNameProviders which return the names to reported in case method parameter constraints are violated but there is nothing comparable for property constraints.

Jea answered 18/9, 2013 at 21:41 Comment(3)
I've filed issue HV-823 for supporting this use case in Hibernate Validator.Jea
Bummer! But also Thank you for opening a feature request.Boggs
This is now supported by Hibernate Validator. Documentation: docs.jboss.org/hibernate/stable/validator/reference/en-US/…Inkblot
S
0

I have raised this issue as I am using problem-spring-web module to do the validation, and that doesn't support bean definition names out of box as hibernate. so I have came up with the below logic to override the createViolation of ConstraintViolationAdviceTrait and fetch the JSONProperty field name for the field and create violations again.

public class CustomBeanValidationAdviceTrait implements ValidationAdviceTrait {

    private final ObjectMapper objectMapper;

    public CustomBeanValidationAdviceTrait(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }


 @Override
    public Violation createViolation(ConstraintViolation violation) {
        String propertyName = getPropertyName(violation.getRootBeanClass(), violation.getPropertyPath().toString());
        return new Violation(this.formatFieldName(propertyName), violation.getMessage());
    }


 private String getPropertyName(Class clazz, String defaultName) {
        JavaType type = objectMapper.constructType(clazz);
        BeanDescription desc = objectMapper.getSerializationConfig().introspect(type);
        return desc.findProperties()
                .stream()
                .filter(prop -> prop.getInternalName().equals(defaultName))
                .map(BeanPropertyDefinition::getName)
                .findFirst()
                .orElse(defaultName);
    }
Silverware answered 29/3, 2021 at 22:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.