Search code examples
jsonscalatypeclassimplicitupickle

Scala 2.13: Case class with extendable variable attributes?


I want to create a case class which can incorporate a record of string and another case class entity.

For example:

case class Student(
name: String
age: Int
)
case class Example(
[key:String]: Student
)

Now I want to use Example to add multiple attributes where attribute could have N number of elements however the type of all those attributes would remain Student. Here's an example:

Example(student1 = Student("name",12),student2=Student("name2",13))

Reason why I am using Case class is that I need to transform this into a JSON using UPickle library and so I wanted to know on the feasibility of achieving the same.

Please note that Example class not just contains [key:String]: Student attribute types but also somethings like:

case class Example(
[key:String]: Student,
_logOp: Option[Boolean] = false,
queryName: String,
...
)

The transformed result for case class:

case class Example(
_logOp: String,
variation: String,
[key:String]: FiltersCaseClass 
/* This line I have added to simplify and make my problem more understandable. Basically the case class would contain some properties like `_logOp` `variation` and then a lot of keys with their values as another case class `FilterCaseClass`
*/
)

should look something like this:

{"_logOp":"AND","variation": "en","ids": {"_logOp": "OR","_expressions": [{"value": "242424"},{"value": "242422"}]}}

where FilterCaseClass is:

case class FilterCaseClass(
_logOp: String,
_expressions: Seq[SingleValueFilter]
)

where SingleValueFilter is another case class containing values

Edit 1:

As per one of the answers by Dymtro:

case class Example(
  m: Map[String, Student],
  _logOp: Option[Boolean] = Some(false),
  queryName: String
)
object Example {
  implicit val rw: ReadWriter[Example] = macroRW
}

write(Example(
  Map("student1" -> Student("name",12), "student2" -> Student("name2",13)),
  Some(true),
  "abc"
))
//{"m":{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13}},"_logOp":[true],"queryName":"abc"}

The only difference I want here is:

{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13},"_logOp":[true],"queryName":"abc"}

The difference is that I want case class to be flexible to add key value pairs of Student class.


Solution

  • You don't need a case class Example, in µPickle you can create a json mixing manual construction and case-class construction

    import upickle.default.{macroRW, ReadWriter, write, transform} // "com.lihaoyi" %% "ujson" % "0.9.6"
    
    case class Student(
      name: String,
      age: Int
    )
    
    object Student {
      implicit val rw: ReadWriter[Student] = macroRW
    }
    
    ujson.Obj(
      "student1" -> write(Student("name",12)), 
      "student2" -> write(Student("name2",13))
    )
    //{"student1":"{\"name\":\"name\",\"age\":12}","student2":"{\"name\":\"name2\",\"age\":13}"}
    
    ujson.Obj(
      "student1" -> transform(Student("name",12)).to[ujson.Value],
      "student2" -> transform(Student("name2",13)).to[ujson.Value]
    )
    //{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13}}
    

    If [key:String]: Student means Map[String, Student] then µPickle seems to support this out-of-the-box

    case class Example(
      m: Map[String, Student],
      _logOp: Option[Boolean] = Some(false),
      queryName: String
    )
    object Example {
      implicit val rw: ReadWriter[Example] = macroRW
    }
    
    write(Example(
      Map("student1" -> Student("name",12), "student2" -> Student("name2",13)),
      Some(true),
      "abc"
    ))
    //{"m":{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13}},"_logOp":[true],"queryName":"abc"}
    

    It shouldn't be nested within m

    You can achieve this with a custom codec (pickler)

    import upickle.default.{ReadWriter, macroRW, readwriter, transform, write, read}
    import scala.collection.mutable
    
    case class Example(
      m: Map[String, Student],
      _logOp: Option[Boolean] = Some(false),
      queryName: String
    )
    object Example {
      implicit val rw: ReadWriter[Example] = {
        val standardExampleRW = macroRW[Example]
        readwriter[ujson.Value].bimap[Example](
          example => transform[Example](example)(standardExampleRW).to[ujson.Value] match {
            case ujson.Obj(standardMap) =>
              val newMap = mutable.LinkedHashMap.empty[String, ujson.Value]
              standardMap.remove("m")
              newMap.addAll(example.m.map { case (str, stud) => str -> transform[Student](stud).to[ujson.Value]})
                .addAll(standardMap)
              ujson.Obj(newMap)
          },
          // if you don't need a reversed transform i.e. from a json to an Example then you can omit this part
          // _ => ??? 
          {
            case ujson.Obj(newMap) =>
              val logOpJson = newMap.remove("_logOp")
              val logOp = logOpJson.map(transform[ujson.Value](_).to[Option[Boolean]])
              val queryNameJson = newMap.remove("queryName")
              val queryName = queryNameJson.map(transform[ujson.Value](_).to[String]).getOrElse("")
              val m = newMap.map { case (str, json) => str -> transform[ujson.Value](json).to[Student] }.toMap
              logOp.map(Example(m, _, queryName)).getOrElse(Example(m, queryName = queryName))
          }
        )
      }
    }
    
    write(Example(
      Map("student1" -> Student("name",12), "student2" -> Student("name2",13)),
      Some(true),
      "abc"
    ))
    //{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13},"_logOp":[true],"queryName":"abc"}
    
    read[Example](
      """{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13},"_logOp":[true],"queryName":"abc"}"""
    )
    //Example(Map(student1 -> Student(name,12), student2 -> Student(name2,13)),Some(true),abc)
    

    So, basically you can generate case classes in Scala but it's not necessary for serialization into json format.


    Just for the completeness, since your original question was how to define a case class, here is a code with actual case-class definition. But this code is slow (since it uses runtime reflection and runtime compilation) and is not conventional Scala code (on contrary to the above custom picklers)

    case class Example(
      m: Map[String, Student],
      _logOp: Option[Boolean] = Some(false),
      queryName: String
    )
    
    import scala.reflect.runtime.{currentMirror => rm} // libraryDependencies += scalaOrganization.value % "scala-reflect" % "2.13.10"
    import scala.reflect.runtime.universe.{Quasiquote, TermName, typeOf, termNames}
    import scala.tools.reflect.{ToolBox, FrontEnd} // libraryDependencies += scalaOrganization.value % "scala-compiler" % "2.13.10"
    val tb = rm.mkToolBox(
    //  frontEnd = new FrontEnd {
    //    override def display(info: Info): Unit = println(info)
    //  },
    //  options = "-d out"
    )
    
    implicit val rw: ReadWriter[Example] =
      readwriter[ujson.Value].bimap[Example](
        example => {
          val studentFields = example.m.keys.map(str =>
            q"val ${TermName(str)}: ${typeOf[Student]}"
          )
          val students = example.m.values.toSeq
          val fields = studentFields ++ Seq(
            q"val _logOp: Option[Boolean] = Some(false)",
            q"val queryName: String"
          )
          val classSymbol = tb.define(q"case class Example1(..$fields)").asClass
          val constructorSymbol =
            classSymbol.typeSignature.decl(termNames.CONSTRUCTOR).asMethod
          val classInstance = tb.mirror.reflectClass(classSymbol)
            .reflectConstructor(constructorSymbol)
            .apply(students ++ Seq(example._logOp, example.queryName): _*)
          tb.eval(q"""
            import upickle.default._
            implicit val rw: ReadWriter[$classSymbol] = macroRW[$classSymbol]
            transform[$classSymbol](_: $classSymbol).to[ujson.Value]
          """).asInstanceOf[Any => ujson.Value].apply(classInstance)
        },
        json => ???
      )
    
    val x = write(Example(
      Map("student1" -> Student("name",12), "student2" -> Student("name2",13)),
      Some(true),
      "abc"
    ))
    //{"student1":{"name":"name","age":12},"student2":{"name":"name2","age":13},"_logOp":[true],"queryName":"abc"}
    

    q"..." is a string interpolator for quasiquotes (creating abstract syntax trees).