Search code examples
scalacirceenumeratum

Enumeratum Circe Serialisation


I have a simple case class like so:

case class ColumnMetadata(name: String,
                          displayName: String,
                          description: Option[String],
                          attributeType: AttributeType)

sealed trait AttributeType extends EnumEntry

case object AttributeType extends Enum[AttributeType] with CirceEnum[AttributeType] {

 val values: immutable.IndexedSeq[AttributeType] = findValues

  case object Number extends AttributeType
  case object Text extends AttributeType
  case object Percentage extends AttributeType
}

And semi-auto encoder:

package object circe {

  implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames


  implicit val columnMetaDecoder: Decoder[ColumnMetadata] = deriveConfiguredDecoder
  implicit val columnMetaEncoder: Encoder[ColumnMetadata] = deriveConfiguredEncoder

  implicit val columnTypeDecoder: Decoder[AttributeType] = deriveConfiguredDecoder
  implicit val columnTypeEncoder: Encoder[AttributeType] = deriveConfiguredEncoder
}

However when I do a serialisation test:

  ColumnMetadata("column1", "Column 1", Some("column1"), 
    AttributeType.withName("Text")).asJson

I get:

  {
    "name" : "column1",
    "display_name" : "Column 1",
    "description" : "column1",
    "attribute_type" : {
      "Text": {}
    }
  }

When I want:

  {
    "name" : "column1",
    "display_name" : "Column 1",
    "description" : "column1",
    "attribute_type" : "Text"
  } 

It works when I use the automatic derivation but I want to use a semi-auto derivation so I can use feature such as withSnakeCaseMemberNames.


Solution

  • This is your error:

    
      implicit val columnTypeDecoder: Decoder[AttributeType] = deriveConfiguredDecoder
      implicit val columnTypeEncoder: Encoder[AttributeType] = deriveConfiguredEncoder
    

    This will derive new codecs, treating AttributeType as any other sealed trait, so it will use discrimination value (semi auto always ignores existing codecs of the type that they are deriving!).

    So you are deriving a new AttributeType codecs, put them in scope where they are used, and so making these new implementations have higher priority than the ones from an companion object. Closer implicit always wins.

    If you won't derive the codecs (because there are already existing implementations provided by CirceEnum trait) then it will work as you expect.

    Additionally instead of doing this:

      implicit val columnMetaDecoder: Decoder[ColumnMetadata] = deriveConfiguredDecoder
      implicit val columnMetaEncoder: Encoder[ColumnMetadata] = deriveConfiguredEncoder
    

    you can just do this:

    // make sure that Configuration is in scope by e.g. importing it
    // or putting it in package object in the same package as case class
    @ConfiguredJsonCodec
    case class ColumnMetadata(name: String,
                              displayName: String,
                              description: Option[String],
                              attributeType: AttributeType)
    

    This will spare you effort of creating package of codecs and importing them manually, everywhere you need them. E.g.

    // imports
    
    package object models_package {
    
      private[models_package] implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
    }
    
    package models_package
    
    // imports
    
    @ConfiguredJsonCodec
    case class ColumnMetadata(name: String,
                              displayName: String,
                              description: Option[String],
                              attributeType: AttributeType)
    
    sealed trait AttributeType extends EnumEntry
    object AttributeType extends Enum[AttributeType] with CirceEnum[AttributeType] {
    
     val values: immutable.IndexedSeq[AttributeType] = findValues
    
      case object Number extends AttributeType
      case object Text extends AttributeType
      case object Percentage extends AttributeType
    }