2

Our Rest API is used by several external parties. They all use "ISO-ish" formats, but the formatting of the time zone offset is slightly different. These are some of the most common formats we see:

  1. 2018-01-01T15:56:31.410Z
  2. 2018-01-01T15:56:31.41Z
  3. 2018-01-01T15:56:31Z
  4. 2018-01-01T15:56:31+00:00
  5. 2018-01-01T15:56:31+0000
  6. 2018-01-01T15:56:31+00

In my controller I use the following annotations:

@RequestMapping(value = ["/some/api/call"], method = [GET])
fun someApiCall(
  @RequestParam("from") 
  @DateTimeFormat(iso = ISO.DATE_TIME) 
  from: OffsetDateTime
) {
  ...
}

It parses variant 1-4 just fine but produces a 400 Bad Request error for variants 5 and 6 with the following exception:

Caused by: java.time.format.DateTimeParseException: Text '2018-01-01T13:37:00.001+00' could not be parsed, unparsed text found at index 23
  at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1952)
  at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)

How can I make it accept all the above ISO formatting variants (even if they are not 100% compliant to the ISO standard)?

Bastian Voigt
  • 5,311
  • 6
  • 47
  • 65

2 Answers2

2

I solved it by adding a custom formatter annotation:

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class IsoDateTime

Plus a FormatterFactory:

class DefensiveDateTimeFormatterFactory : 
EmbeddedValueResolutionSupport(), AnnotationFormatterFactory<IsoDateTime> 
{
  override fun getParser(annotation: IsoDateTime?, fieldType: Class<*>?): Parser<*> {
    return Parser<OffsetDateTime> { text, _ -> OffsetDateTime.parse(text, JacksonConfig.defensiveFormatter) }
  }

  override fun getPrinter(annotation: IsoDateTime, fieldType: Class<*>): Printer<*> {
    return Printer<OffsetDateTime> { obj, _ -> obj.format(DateTimeFormatter.ISO_DATE_TIME) }
  }

  override fun getFieldTypes(): MutableSet<Class<*>> {
    return mutableSetOf(OffsetDateTime::class.java)
  }
}

The actual DateTimeFormat class comes from my other question, How to parse different ISO date/time formats with Jackson and java.time?

And added it to Spring using WebMvcConfigurer:

@Configuration
open class WebMvcConfiguration : WebMvcConfigurer {
  override fun addFormatters(registry: FormatterRegistry) {
    registry.addFormatterForFieldAnnotation(DefensiveDateTimeFormatterFactory())
  }
}
Bastian Voigt
  • 5,311
  • 6
  • 47
  • 65
1

You get 400-Bad Request because format 5 and 6 are not part of the ISO specifications.

If you look at the Time Zone Designator, it can be one of Z or +hh:mm or -hh:mm.

The official list of ISO-8601 formats can be found here.

   Year:
      YYYY (eg 1997)
   Year and month:
      YYYY-MM (eg 1997-07)
   Complete date:
      YYYY-MM-DD (eg 1997-07-16)
   Complete date plus hours and minutes:
      YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
   Complete date plus hours, minutes and seconds:
      YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
   Complete date plus hours, minutes, seconds and a decimal fraction of a second
      YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)

where:

     YYYY = four-digit year
     MM   = two-digit month (01=January, etc.)
     DD   = two-digit day of month (01 through 31)
     hh   = two digits of hour (00 through 23) (am/pm NOT allowed)
     mm   = two digits of minute (00 through 59)
     ss   = two digits of second (00 through 59)
     s    = one or more digits representing a decimal fraction of a second
     TZD  = time zone designator (Z or +hh:mm or -hh:mm)
Raja Anbazhagan
  • 4,092
  • 1
  • 44
  • 64
  • I guess you're right, but nevertheless our clients use these formats (probably they use some broken frameworks/libraries). If possible I would like to accept all formats to give our clients the best possible API experience even if they use broken libraries. – Bastian Voigt May 07 '18 at 14:31
  • You may try searching for `@InitBinder`. This way you can supply your own converter mechanism. May be this post might help. https://stackoverflow.com/questions/5211323/what-is-the-purpose-of-init-binder-in-spring-mvc?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa – Raja Anbazhagan May 07 '18 at 14:46
  • 1
    According to wikipedia, format 6 should be covered by iso8601 – Bastian Voigt May 07 '18 at 14:47
  • I wouldn't trust a wikipedia page over a `w3.org` published document. – Raja Anbazhagan May 07 '18 at 15:09
  • Me neither, but also W3C is not ISO. The real ISO standard document is not available for free :( – Bastian Voigt May 07 '18 at 15:11