I am using Circe for json operations. I have added custom encoders and decoders to handle some of the types, like Joda Time.
While parsing DateTime, I want to allow multiple formats to be passed.
For eg. dd-MM-yyyy'T'HH:mm:ss'Z'
and dd-MM-yyyy'T'HH:mm:ss.SSS'Z'
I have defined my decoder like below:
val dateTimeFormat = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
val dateTimeFormatWithMillis = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
implicit val jodaDateTimeFormat: Encoder[DateTime] with Decoder[DateTime] = new Encoder[DateTime] with Decoder[DateTime] {
override def apply(a: DateTime): Json = Encoder.encodeString(a.toString("yyyy-MM-dd'T'HH:mm:ss'Z'"))
override def apply(c: HCursor): Result[DateTime] = Decoder.decodeString.map { x =>
DateTime.parse(x, dateTimeFormat)
}.apply(c)
}
Now if i input a datetime string matching the dateTimeFormat
, then the decoding will work, but if I pass the datetime in dateTimeFormatWithMillis
, it will fail to process.
I know that I can use the DateTimeFormatterBuilder
to add multiple parsers and process it, however, I was wondering if there is a way in Circe to chain multiple decoders to try one after another until it succeeds or reached end of chain?
You can use Decoder#or
to combine decoders so that the second one is tried in case the first one fails.
Here is a working example:
import org.joda.time.DateTime
import org.joda.time.format.{DateTimeFormat, DateTimeFormatter}
import io.circe.{Decoder, Encoder}
import io.circe.parser.decode
import scala.util.Try
val dateTimeFormat = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
val dateTimeFormatWithMillis = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
/** Creates a decoder that decodes a [[DateTime]] using the provided format. */
def dateTimeFormatDecoder(format: DateTimeFormatter): Decoder[DateTime] =
Decoder[String].emapTry(str => Try(DateTime.parse(str, format)))
/** [[Decoder]] for the first format (without milliseconds). */
val dateTimeWithoutMillisDecoder: Decoder[DateTime] =
dateTimeFormatDecoder(dateTimeFormat)
/** [[Decoder]] for the second format (with milliseconds). */
val dateTimeWithMillisDecoder: Decoder[DateTime] =
dateTimeFormatDecoder(dateTimeFormatWithMillis)
/** Encodes a [[DateTime]] using `Encoder[String].contramap(...)`, which is
* perhaps a slightly more idiomatic version of
* `Encoder.encodeString(a.toString("yyyy-MM-dd'T'HH:mm:ss'Z'"))` */
implicit val jodaDateTimeEncoder: Encoder[DateTime] =
Encoder[String].contramap(_.toString("yyyy-MM-dd'T'HH:mm:ss'Z'"))
implicit val jodaDateTimeDecoder: Decoder[DateTime] =
dateTimeWithoutMillisDecoder or dateTimeWithMillisDecoder
println(decode[DateTime](""" "2001-02-03T04:05:06Z" """))
println(decode[DateTime](""" "2001-02-03T04:05:06.789Z" """))
Note that the Encoder
and Decoder
have been separated since Decoder#or
returns a Decoder
, which wouldn’t work with a combined class (i.e. Encoder[DateTime] with Decoder[DateTime]
).
Also, the DateTime.parse
calls have been wrapped with Decoder#emapTry
because the or
combinator (and in general all Decoder
combinators) expects to be dealing with Either
values, not exceptions.