Question

I am generating a set of methods that will instantiate a class from a Play JsObject, e.g.

class Clazz(val1: Int = 1, val2: String, val3: Option[Int])

def createFromJson(json: JsObject) {
  Clazz((json \ "val1").as[Int], (json \ "val2").as[String], (json \ "val3").as[Option[Int]])
}

Several of our classes have more than 22 fields, so it's not feasible to use a default Writes/Formats to do this (and anyway, the default value problem would remain).

I would like to be able to say e.g. (json \ "val1").asOpt[Int].getOrElse(1) for parameters that have default values. I could create a val defaultValues: Map[String, JsValue] variable with all of the class's default values, or I could create a val defaultValues: JsObject that I merge with the input json, but ideally I'd like to be able to pull the default value directly from the class, otherwise we'll inevitably update the constructor's default values but not the defaultValues variable's default values or vice versa.

Is there a way to do this?

Was it helpful?

Solution

There's always a way :)

"Pulling default from the class" sounds like runtime reflection work or macro work to me - neither is a simple task if you don't have any prior experience with it.

There seems to be a reflection solution here which you may be able to adapt: How do I access default parameter values via Scala reflection?

But that has runtime performance cost and may impact your expected deserialization speed.

I would recommend going with the defaultValues map and creating apply methods on the companion object that handle the default value logic. It will get the job done.

OTHER TIPS

I suggest you to use case class on scala 2.11: there is no more the 22 fields limit. You can do that via macro or reflection. The default value for a member can be accessed with the apply$default$ of on the module.

This a sample code that extracts the default values and returns a map of [String,Any] of defaults.

  def getDefault[T](c: Context)(T: c.WeakTypeTag[T]): c.Expr[Map[String, Any]] = {
    import c.universe._
    val classSym = T.tpe.typeSymbol
    val moduleSym = classSym.companionSymbol
    val apply = moduleSym.typeSignature.declaration(newTermName("apply")).asMethod
    // can handle only default parameters from the first parameter list
    // because subsequent parameter lists might depend on previous parameters
    val kvps = apply.paramss.head.map(_.asTerm).zipWithIndex.flatMap {
      case (p, i) =>
        if (!p.isParamWithDefault) None
        else {
          val getterName = newTermName("apply$default$" + (i + 1))
          Some(q"${p.name.toString} -> $moduleSym.$getterName")
        }
    }
    c.Expr[Map[String, Any]](q"Map[String, Any](..$kvps)")
  }

Note in your sample code a "new" is missing.

Hi, Alberto

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top