Search code examples
javaspringvalidationhibernate-validator

Using validation in Spring to restrict ZonedDateTime field to 3 digits at millisecond time


I have an input DTO in a Spring MVC request that I want to validate, specifically the ZonedDateTime field should at most contain 3 digits at millisecond level, i.e. it cannot be the full nanoseconds precision. Other than that the input request should follow the ISO datetime format. I could make the field a String and then just restrict it with a regex but I prefer to keep it as an ZonedDateTime so I don't need to parse it again.

The object looks like this:

@Data
public class MeasurementDTO {

    private ZonedDateTime date;
    @Digits(integer = 20, fraction = 8) private BigDecimal value;

}

And this is a nested DTO where the parent object comes in as a @RequestBody with the @Valid annotation.

I've tried @JsonFormat but I can't seem to restrict the millisecond part. Is there any way to do this or should I just parse it myself as a String and then deal with it? Or even just leave it at ZonedDateTime and then check the nanosecond component to see if it's there in a custom validator?

Thanks to Tim's answer I remembered Java Dates don't have more precision than millis anyway, so I updated the question to use ZonedDateTimes which do have up to nanosecond precision. I do want to be able to give the user a warning if they do attempt to pass more precision, if using Date this information would just get swallowed.


Solution

  • I did this the hard way with a custom validator. I am still welcoming answers that do this in a more concise way though.

    Here's my solution:

    I added an annotation @ValidMeasurementInput

    @Documented
    @Constraint(validatedBy = MeasurementValidator.class)
    @Target({TYPE, FIELD, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    public @interface ValidMeasurementInput {
    
        String message() default "Invalid measurement input";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    

    And implemented a custom validator

    public class MeasurementValidator implements ConstraintValidator<ValidMeasurementInput, MetricsDTO> {
    
        @Override
        public boolean isValid(MetricsDTO metricsDTO, ConstraintValidatorContext context) {
            ...
        }
    }
    

    Somewhere in this class among some other validations is this code:

       int nano = measurementDTO.getDate().getNano();
       int remainderAfterMillis = nano % 1000000;
       if (remainderAfterMillis != 0)
           valid = false;
    

    And of course I added the @ValidMeasurementInput to my DTO.