ZonedTime fromJSON toJSON

309 views Asked by At

I am befuzzled by Aeson and Servant conversions of ZonedTime.

To my Servant app I give some time in the url: .../2016-12-18T07:51:00+03:00/....

Servant easily converts it to ZonedTime with ... :> Capture "zt" ZonedTime :> ....

Then my app does some calculations and in json-response I want to give back to the client this and some other ZonedTimes — in case if client wants to give these times to my app again.

If input timezone was not zero +0X:00 (X /= 0), then on the output I also get +0X:00, but if on input I give .../2016-12-18T07:51:00+00:00/..., then in response I get 2016-12-18T07:51:00Z. And if I try to feed this string to Servant again with .../2016-12-18T07:51:00Z/..., then Servant fails to convert it to ZonedTime. Actually is returns HTTP 400 (Bad Request).

Why? What for?

2

There are 2 answers

1
K. A. Buhr On

ISO 8601 is the standard textual representation for times used in JSON. When the timezone is UTC, either "+00:00" or "Z" is a valid timezone suffix. It looks like Aeson outputs times in ISO 8601 using the "Z" suffix if it's UTC and a "+xx:xx" suffix otherwise. Unfortunately, Servant (actually, Web.HttpApiData) uses a naive method of parsing ZonedTime from URL pieces that doesn't permit the "Z" suffix. If you parse a UTCTime instead, then it uses (and requires) the "Z" suffix.

You can define a newtype alias for ZonedTime as follows that handles both formats by trying to parse as a ZonedTime and -- failing that -- a UTCTime with conversion:

module ZonedTimeTest where

import Data.Time.LocalTime
import Servant.API

newtype ZonedTime' = ZonedTime' { getZonedTime :: ZonedTime }

instance FromHttpApiData ZonedTime' where
  parseUrlPiece u =     ZonedTime' <$> parseUrlPiece u            -- "...+xx:xx"
                    <!> ZonedTime' . fromUTC <$> parseUrlPiece u  -- "...Z"
    where fromUTC = utcToZonedTime utc
          infixl 3 <!>
          Left _ <!> y = y
          x      <!> _ = x

and then Capture "zt" ZonedTime' should handle both formats for you (though you'll need to unwrap the ZonedTime' to a ZonedTime where appropriate).

0
Dahan On

Ok.

So, as soon as ...Z notation is properly parsed by Servant as UTCTime, I just made another capturing line:

:<|> "myendpoint" :> Capture "zt" ZonedTime :> ...
:<|> "myendpoint" :> Capture "utct" UTCTime :> ...

And made corresponding handlers. With this literals with +xx:xx get taken as ZonedTime and go to one handler, and literals with Z are taken by second line and go to second handler, that does the same as the first one, but converts UTC to Zoned on the fly.


Update

I came to understand, that the way, how my system is working leads to the fact, that in the URL I may be having different variations of string encoding time.

  1. As a timezone may be +03:00 or just Z if it is for UTC.
  2. I may have fractional seconds like 2016-12-09T15:04:26.349857693845+05:00

Standard Capture for ZonedTime doesn't understand neither Z as timezone, nor fractional seconds.

So I did somehow in the direction, as proposed in another Answer here.

I put only one Capturing line

"daymonth" :> Capture "zt" ZonedTime' :> Capture "fl" Double :> Get '[JSON] Value

and I make new type ZonedTime', that is doing everything for me.

newtype ZonedTime' = ZonedTime' { unwrap :: ZonedTime }

instance FromHttpApiData ZonedTime' where
  parseUrlPiece text = Right zt
    where
      strRaw = unpack text
      str = subRegex (mkRegex "Z$") strRaw "+00:00"
      zt = ZonedTime' (parseTimeOrError False defaultTimeLocale "%Y-%m-%dT%H:%M:%S%Q%z" str)
  1. I deal with Z problem, substituting it with +00:00
  2. Then I write parsing of the string manually, giving the format string, where %Q catches fractional part of seconds.