Skip to content

Instantly share code, notes, and snippets.

@BalmungSan
Last active February 19, 2022 08:00
Show Gist options
  • Save BalmungSan/3ad8513252741ef93822a334e8f3798a to your computer and use it in GitHub Desktop.
Save BalmungSan/3ad8513252741ef93822a334e8f3798a to your computer and use it in GitHub Desktop.
Custom MongoDB codec for reading/writing ZonedDateTime instances in UTC
import cats.effect.IO
import io.circe.Encoder
import io.circe.generic.semiauto.deriveEncoder
import io.circe.java8.time.encodeZonedDateTimeDefault
import io.circe.syntax.EncoderOps
import org.bson.{BsonReader, BsonType, BsonWriter}
import org.bson.codecs.{DecoderContext, Codec, EncoderContext}
import org.bson.codecs.configuration.{CodecConfigurationException, CodecRegistries}
import org.mongodb.scala.MongoClient
import org.mongodb.scala.bson.codecs.{DEFAULT_CODEC_REGISTRY, Macros}
import org.mongodb.scala.bson.collection.immutable.Document
import java.time.{Instant, ZonedDateTime, ZoneOffset}
/** Custom codec for encoding and decoding ZonedDateTime instances zoned in UTC */
object UTCZonedDateTimeMongoCodec extends Codec[ZonedDateTime] {
@throws(classOf[CodecConfigurationException])
override def decode(reader: BsonReader, decoderContext: DecoderContext): ZonedDateTime = reader.getCurrentBsonType match {
case BsonType.DATE_TIME =>
Instant.ofEpochMilli(reader.readDateTime).atZone(ZoneOffset.UTC)
case t =>
throw new CodecConfigurationException(s"Could not decode into ZonedDateTime, expected DATE_TIME BsonType but got '$t'.")
}
@throws(classOf[CodecConfigurationException])
override def encode(writer: BsonWriter, value: ZonedDateTime, encoderContext: EncoderContext): Unit = {
try {
writer.writeDateTime(value.withZoneSameInstant(ZoneOffset.UTC).toInstant.toEpochMilli)
} catch {
case e: ArithmeticException =>
throw new CodecConfigurationException(
s"Unsupported LocalDateTime value '$value' could not be converted to milliseconds: ${e.getMessage()}",
e
)
}
}
override def getEncoderClass(): Class[ZonedDateTime] = classOf[ZonedDateTime]
}
/** Simple case class with a ZonedDateTime field */
final case class Data(
id: Int,
name: String,
date: ZonedDateTime
)
/** Circe JSON encoder for our simple case class */
implicit val dataCirceEncoder: Encoder[Data] = deriveEncoder
/** Mongo codec registry which supports ZonedDateTime and Data instances */
val mongoCodecRegistry =
CodecRegistries.fromRegistries(
CodecRegistries.fromCodecs(UTCZonedDateTimeMongoCodec),
CodecRegistries.fromProviders(Macros.createCodecProvider[Data]),
DEFAULT_CODEC_REGISTRY
)
/** Simple program for testing the codec */
val program = for {
client <- IO { MongoClient() } // mongodb://host:27017
db = client.getDatabase("test").withCodecRegistry(mongoCodecRegistry)
col = db.getCollection[Data]("data")
testData = Data(
id = 3,
name = "BalmungSan",
date = ZonedDateTime.parse("1997-05-23T13:00:00+05:00[America/Bogota]")
)
_ <- IO.fromFuture { IO { col.deleteMany(Document.empty).toFuture } }
_ <- IO.fromFuture { IO { col.insertOne(testData).toFuture } }
result <- IO.fromFuture { IO { col.find().first().toFuture } }
} yield result.asJson.noSpaces
val jsonData = program.unsafeRunSync() // "{"id":3,"name":"BalmungSan","date":"1997-05-23T18:00:00Z"}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment