2

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?

Yadu Krishnan
  • 3,492
  • 5
  • 41
  • 80

1 Answers1

3

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.

Olli Helenius
  • 439
  • 4
  • 5