20

The question here asks about mapping a case class to a Map[String,Any]. I was wondering what would be the other way around, converting Map[String,Any] to a case class. Given the following map:

val mp = Map("name" -> "Tom", "address" -> Map("street" -> "Jefferson st", "zip" -> 10000))

Convert it to a case class of Person:

case class Person(name:String, address:Address)
case class Address(street:String, zip:Int)

val p = Person("Tom", Address("Jefferson st", 10000))

with something like this:

val newP = mp.asCC[Person]
assert(newP.get == p)

How should I do that with Shapeless.

Community
  • 1
  • 1
Omid
  • 1,959
  • 25
  • 42

1 Answers1

30

Here's an off-the-cuff, mostly untested solution. First for the type class:

import shapeless._, labelled.{ FieldType, field }

trait FromMap[L <: HList] {
  def apply(m: Map[String, Any]): Option[L]
}

And then the instances:

trait LowPriorityFromMap {
  implicit def hconsFromMap1[K <: Symbol, V, T <: HList](implicit
    witness: Witness.Aux[K],
    typeable: Typeable[V],
    fromMapT: Lazy[FromMap[T]]
  ): FromMap[FieldType[K, V] :: T] = new FromMap[FieldType[K, V] :: T] {
    def apply(m: Map[String, Any]): Option[FieldType[K, V] :: T] = for {
      v <- m.get(witness.value.name)
      h <- typeable.cast(v)
      t <- fromMapT.value(m)
    } yield field[K](h) :: t
  }
}

object FromMap extends LowPriorityFromMap {
  implicit val hnilFromMap: FromMap[HNil] = new FromMap[HNil] {
    def apply(m: Map[String, Any]): Option[HNil] = Some(HNil)
  }

  implicit def hconsFromMap0[K <: Symbol, V, R <: HList, T <: HList](implicit
    witness: Witness.Aux[K],
    gen: LabelledGeneric.Aux[V, R],
    fromMapH: FromMap[R],
    fromMapT: FromMap[T]
  ): FromMap[FieldType[K, V] :: T] = new FromMap[FieldType[K, V] :: T] {
    def apply(m: Map[String, Any]): Option[FieldType[K, V] :: T] = for {
      v <- m.get(witness.value.name)
      r <- Typeable[Map[String, Any]].cast(v)
      h <- fromMapH(r)
      t <- fromMapT(m)
    } yield field[K](gen.from(h)) :: t
  }
}

And then a helper class for convenience:

class ConvertHelper[A] {
  def from[R <: HList](m: Map[String, Any])(implicit
    gen: LabelledGeneric.Aux[A, R],
    fromMap: FromMap[R]
  ): Option[A] = fromMap(m).map(gen.from(_))
}

def to[A]: ConvertHelper[A] = new ConvertHelper[A]

And the example:

case class Address(street: String, zip: Int)
case class Person(name: String, address: Address)

val mp = Map(
  "name" -> "Tom",
  "address" -> Map("street" -> "Jefferson st", "zip" -> 10000)
)

And finally:

scala> to[Person].from(mp)
res0: Option[Person] = Some(Person(Tom,Address(Jefferson st,10000)))

This will only work for case classes whose members are either Typeable or other case classes whose members are either Typeable or other case classes… (and so on).

Gotcha

Careful not to have scala.reflect.runtime.universe._ in your imports, as this will break the above.

samthebest
  • 30,803
  • 25
  • 102
  • 142
Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Changing the example as follows it does not seem to work. Any idea? `case class Address(street: String, zip: Int, state: String)` `val mp = Map( "name" -> "Tom", "address" -> Map("street" -> "Jefferson st", "zip" -> 10000, "state" -> "CA") )` – lambdista Sep 28 '16 at 13:27
  • @lambdista It's was a divergence issue—I've just edited the answer to be more robust. – Travis Brown Sep 28 '16 at 14:00
  • @TravisBrown How can this be adapted to allow for some of the values in the Map to be "null". For instance `"zip" -> null`. – ISJ Aug 09 '17 at 16:29
  • Got it working. Mapped over `v <- m.get(witness.value.name)` and if it returns a Some(null), pass None on. Also changed the Map to `Map[String, Option[Any]]`. Case Class types also wrapped in Options. – ISJ Aug 09 '17 at 20:47
  • I'm using latest version of shapeless (2.3.3) and this doesn't compile `v <- m.get(witness.value.name)`. – samthebest Mar 04 '19 at 16:59
  • @samthebest I just did a quick run with 2.3.3 and that part compiles just fine for me. You should probably open a new question with details about your Scala version, etc. – Travis Brown Mar 04 '19 at 17:05
  • @TravisBrown Thanks, ok https://stackoverflow.com/questions/54988267/why-does-this-shapeless-code-not-compile – samthebest Mar 04 '19 at 17:13
  • @TravisBrown it doesn't seem to work when we have a nested structure, where the substructure is nested in an Option https://stackoverflow.com/questions/55042252/shapeless-code-to-convert-mapstring-any-to-case-class-cannot-handle-optional please can you help? – samthebest Mar 07 '19 at 11:02