0

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.

Sebastiaan van den Broek
  • 5,818
  • 7
  • 40
  • 73
  • If you want greater than millisecond resolution, use `java.time.Instant` with much finer resolution of [nanoseconds](https://en.wikipedia.org/wiki/Nanosecond). Best to always avoid `java.util.Date` entirely as it is crippled by bad design choices. To lop off any microseconds or nanos in an `Instant`, call the [`truncatedTo`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Instant.html#truncatedTo(java.time.temporal.TemporalUnit)) method. Pass [`ChronoUnit.MILLIS`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/temporal/ChronoUnit.html#MILLIS). – Basil Bourque Mar 11 '19 at 22:24
  • @BasilBourque I already edited Instant into my answer. I actually do not want greater than millisecond resolution for this case, but I would like to be able to detect it if users try to pass it. – Sebastiaan van den Broek Mar 12 '19 at 02:38

2 Answers2

1

You may not consider this to be a complete answer, but java.util.Date only stores precision up to milliseconds, and not beyond that. See Jon Skeet's answer here, or read the source code for CalendarDate, which reveals that it has nothing beyond millisecond storage.

So, there is no sense in restricting a Date to millisecond precision with Hibernate validation, because the type itself already carries this restriction.

Tim Biegeleisen
  • 502,043
  • 27
  • 286
  • 360
  • That is actually a very good point! I was porting this code from a python function where I was using a monstrous regex, but it's not even needed here. Leaving the question open for a while to see if there are any answers that do allow validating part of the time though, even for example restricting on seconds. – Sebastiaan van den Broek Mar 11 '19 at 12:52
  • I decided to use ZonedDateTime in my DTO anyway because I don't want to silently swallow the extra precision. – Sebastiaan van den Broek Mar 12 '19 at 05:29
0

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.

Sebastiaan van den Broek
  • 5,818
  • 7
  • 40
  • 73