8

I do validation with JSR-303 in my Spring app, it works as needed.

This is an example:

@Column(nullable = false, name = "name")
    @JsonProperty("customer_name")
    @NotEmpty
    @Size(min = 3, max = 32)
    private String name;

And REST API clients use customer_name as name of input field that send to API bud validation field error org.springframework.validation.FieldError returns name as name of the field.

Is there some way hot to get JSON-ish name that is specified in @JsonProperty? Or do I have to implement own mapper to map class fields name into its JSON alternative?

Edit1: Renaming class fields into names that correspond to JSON names is not alternative (for many reasons).

jnemecz
  • 3,171
  • 8
  • 41
  • 77
  • This [answer](https://stackoverflow.com/a/44281799/1426227) may give you some insights on how to parse a constraint violation and then use Jackson to find the actual JSON property name. – cassiomolin Aug 24 '17 at 10:23

4 Answers4

5

This can now be done by using PropertyNodeNameProvider.

  • From my initial experimentation, this works for customizing the bean validation `ConstraintValidation` property names, but fails when Spring converts it to a `FieldError`, since it expects the actual field name and not the JSON property name. – M. Justin Mar 15 '21 at 18:19
  • @M.Justin that is true. I opened an issue: https://github.com/spring-projects/spring-framework/issues/24811 for that, but no response yet. The workaround is to extend `org.springframework.validation.beanvalidation.LocalValidatorFactoryBean`, and override `getRejectedValue` to return empty string. This works only if you later (in some @ControllerAdvice) create your own error representation, and not depend on the Spring provided one. – Damir Alibegovic Mar 17 '21 at 09:13
  • For FieldErrors the only solution I found was to access the private ConstraintViolation from Spring ViolationFieldError via reflection. See the Answer I added. – Hollerweger Jun 02 '22 at 12:51
0

There is no way to achieve this currently. We have an issue for this in the reference implementation: HV-823.

This would address the issue on the side of Hibernate Validator (i.e. return the name you expect from Path.Node#getName()), it'd require some more checking whether Spring actually picks up the name from there.

Maybe you'd be interested in helping out with implemeting this one?

Gunnar
  • 18,095
  • 1
  • 53
  • 73
  • A while ago I wrote this [answer](https://stackoverflow.com/a/44281799/1426227). It's about how to parse a constraint violation and then use Jackson to find the actual JSON property name. – cassiomolin Aug 24 '17 at 10:30
  • 2
    Spring got the "field" name from `SpringValidatorAdapter.determineField(ConstraintViolation violation)`, and as per Javadoc: `The default implementation returns the stringified property path`. By default in Spring application, the actual instance of `SpringValidatorAdapter` is of subclass `LocalValidatorFactoryBean`. – p3consulting May 16 '18 at 12:57
0

For MethodArgumentNotValidException and BindException I have written a method that tries to access the private ConstraintViolation from Spring ViolationFieldError via reflection.

  /**
   * Try to get the @JsonProperty annotation value from the field. If not present then the
   * fieldError.getField() is returned.
   * @param fieldError {@link FieldError}
   * @return fieldName
   */
  private String getJsonFieldName(final FieldError fieldError) {
    try {
      final Field violation = fieldError.getClass().getDeclaredField("violation");
      violation.setAccessible(true);
      var constraintViolation = (ConstraintViolation) violation.get(fieldError);
      final Field declaredField = constraintViolation.getRootBeanClass()
          .getDeclaredField(fieldError.getField());
      final JsonProperty annotation = declaredField.getAnnotation(JsonProperty.class);
      //Check if JsonProperty annotation is present and if value is set
      if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) {
        return annotation.value();
      } else {
        return fieldError.getField();
      }
    } catch (Exception e) {
      return fieldError.getField();
    }
  }

This code can be used in methods handling BindExceptions @ExceptionHandler(BindException.class) within a Class with @ControllerAdvice:

@ControllerAdvice
public class ControllerExceptionHandler {

  @ExceptionHandler(BindException.class)
  public ResponseEntity<YourErrorResultModel> handleBindException(final BindException exception) {
    for (FieldError fieldError : exception.getBindingResult().getFieldErrors()) {
      final String fieldName = getJsonFieldName(fieldError);
   ...
}
Hollerweger
  • 975
  • 1
  • 13
  • 32
  • You don't have to use introspection to get at the `violation`: just use `if (fieldError.contains(ConstraintViolation.class) { var violation = fieldError.unwrap(ConstraintViolation.class); ...}` – rem Dec 02 '22 at 12:26
0

Here is the function that gets value from @JsonProperty annotation.

private String getJsonPropertyValue(final FieldError error) {
    try {
        if (error.contains(ConstraintViolation.class)) {
            final ConstraintViolation<?> violation = error.unwrap(ConstraintViolation.class);
            final Field declaredField = violation.getRootBeanClass().getDeclaredField(error.getField());
            final JsonProperty annotation = declaredField.getAnnotation(JsonProperty.class);

            if (annotation != null && annotation.value() != null && !annotation.value().isEmpty()) {
                return annotation.value();
            }
        }
    } catch (Exception ignored) {
    }

    return error.getField();
}

Then in your exception handler

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<?> validationExceptionHandler(MethodArgumentNotValidException e) {
    final Map<String, String> errors = new HashMap<>();
    e.getBindingResult().getAllErrors().forEach((error) -> {
        String fieldName = getJsonPropertyValue((FieldError) error);
        String errorMessage = error.getDefaultMessage();
        errors.put(fieldName, errorMessage);
    });
    System.out.println(errors); // put this in your response
    return ResponseEntity.badRequest().build();
}
Chhaileng
  • 2,428
  • 1
  • 27
  • 24