3

I'm using lift-json to de-serialize JSON strings into Scala case classes. The JSON I'm parsing has a common structure: a data, success and error field, where data contains the interesting bits. I've created an APIResponse class to account for this structure, and a simple parse method which takes a type parameter for the type of the object contained in 'data':

import net.liftweb.json.Serialization.read
import net.liftweb.json.DefaultFormats

object JSONParseTest extends App {
  implicit val formats = DefaultFormats

  def parse[T: Manifest](json: String) = {
    read[APIResponse[T]](json)
  }

  val resultA = parse[TypeA](""" { "data": { "foo": "string" }, "success": true } """)
  println(resultA)
  val resultB = parse[TypeB](""" { "data": { "bar": "string" }, "success": true } """)
  println(resultB)
}

case class TypeA(foo: String)
case class TypeB(bar: String)

case class APIResponse[D](data: D, success: Boolean, error: Option[String]) {
  override def toString: String =
    if(success) {
      "SUCCESSFUL: " + data.toString
    } else {
      "ERROR: " + error.get
    }
}

Everything works great...for the first object parsed. For some reason, though, the second object parsed seems to get "stuck", performing as if it was passed the first type parameter instead of the second. As you can see in the output below, lift-json is looking for a 'foo' field in the JSON string and can't find it; 'foo' exists on TypeA, but not on TypeB. I've checked the manifest in the parse method, and it's got the proper type there. If I comment out the first parse/print, the second starts working. Any pointers on why this isn't behaving as expected? This is sorta boggling my mind.

SUCCESSFUL: TypeA(string)

Exception in thread "main" net.liftweb.json.MappingException: No usable value for data
No usable value for foo
Did not find value which can be converted into java.lang.String
    at net.liftweb.json.Meta$.fail(Meta.scala:191)
    at net.liftweb.json.Extraction$.mkValue$1(Extraction.scala:357)
    at net.liftweb.json.Extraction$.build$1(Extraction.scala:317)
    at net.liftweb.json.Extraction$$anonfun$12.apply(Extraction.scala:253)
    at net.liftweb.json.Extraction$$anonfun$12.apply(Extraction.scala:253)
    at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:233)
    at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:233)
    at scala.collection.LinearSeqOptimized$class.foreach(LinearSeqOptimized.scala:59)
    at scala.collection.immutable.List.foreach(List.scala:76)
    at scala.collection.TraversableLike$class.map(TraversableLike.scala:233)
    at scala.collection.immutable.List.map(List.scala:76)
    at net.liftweb.json.Extraction$.instantiate$1(Extraction.scala:253)
    at net.liftweb.json.Extraction$.newInstance$1(Extraction.scala:286)
    at net.liftweb.json.Extraction$.build$1(Extraction.scala:315)
    at net.liftweb.json.Extraction$.net$liftweb$json$Extraction$$extract0(Extraction.scala:366)
    at net.liftweb.json.Extraction$.net$liftweb$json$Extraction$$extract0(Extraction.scala:199)
    at net.liftweb.json.Extraction$.extract(Extraction.scala:43)
    at net.liftweb.json.JsonAST$JValue.extract(JsonAST.scala:300)
    at net.liftweb.json.Serialization$.read(Serialization.scala:58)
    at jmullin.api.Test$.parse(API.scala:11)
    at jmullin.api.Test$delayedInit$body.apply(API.scala:16)
    at scala.Function0$class.apply$mcV$sp(Function0.scala:34)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App$$anonfun$main$1.apply(App.scala:60)
    at scala.App$$anonfun$main$1.apply(App.scala:60)
    at scala.collection.LinearSeqOptimized$class.foreach(LinearSeqOptimized.scala:59)
    at scala.collection.immutable.List.foreach(List.scala:76)
    at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:30)
    at scala.App$class.main(App.scala:60)
    at jmullin.api.Test$.main(API.scala:7)
    at jmullin.api.Test.main(API.scala)
Caused by: net.liftweb.json.MappingException: No usable value for foo
Did not find value which can be converted into java.lang.String
    at net.liftweb.json.Meta$.fail(Meta.scala:191)
    at net.liftweb.json.Extraction$.mkValue$1(Extraction.scala:357)
    at net.liftweb.json.Extraction$.build$1(Extraction.scala:317)
    at net.liftweb.json.Extraction$$anonfun$12.apply(Extraction.scala:253)
    at net.liftweb.json.Extraction$$anonfun$12.apply(Extraction.scala:253)
    at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:233)
    at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:233)
    at scala.collection.LinearSeqOptimized$class.foreach(LinearSeqOptimized.scala:59)
    at scala.collection.immutable.List.foreach(List.scala:76)
    at scala.collection.TraversableLike$class.map(TraversableLike.scala:233)
    at scala.collection.immutable.List.map(List.scala:76)
    at net.liftweb.json.Extraction$.instantiate$1(Extraction.scala:253)
    at net.liftweb.json.Extraction$.newInstance$1(Extraction.scala:286)
    at net.liftweb.json.Extraction$.build$1(Extraction.scala:315)
    at net.liftweb.json.Extraction$.mkValue$1(Extraction.scala:351)
    ... 29 more
Caused by: net.liftweb.json.MappingException: Did not find value which can be converted into java.lang.String
    at net.liftweb.json.Meta$.fail(Meta.scala:191)
    at net.liftweb.json.Extraction$.convert(Extraction.scala:403)
    at net.liftweb.json.Extraction$.build$1(Extraction.scala:314)
    at net.liftweb.json.Extraction$.mkValue$1(Extraction.scala:351)
    ... 42 more
  • 1
    I wonder if there is some kind of caching going on that's getting tripped up by the erased type. If you want to put a sbt launchable project on github, I'll take a closer look for you. You might also want to post this to the Lift list, Joni, the Lift JSON maintainer usually responds pretty quickly. – Dave Whittaker Mar 08 '13 at 17:18
  • I did a bit of digging into the lift-json source, and it looks like you're right. There's memoization going on for class -> constructor mappings, and type parameters are ignored at that point. I'll bring it up with Joni. Thanks for the tip! – Justin Mullin Mar 08 '13 at 19:10

1 Answers1

3

It turns out lift-json is doing some memoization of class to constructor mappings behind the scenes, ignoring type args. When the lookup is performed for the same class with different type parameters, the old constructor is returned, causing the confusion seen here. Mystery solved (though the problem isn't).

  • Confirmed this is a bug in lift-json. See issue here: https://github.com/lift/framework/issues/1417, and the corresponding Lift list discussion: https://groups.google.com/d/msg/liftweb/Sdp1tcfoVAA/oOnWqUlEgvgJ – Justin Mullin Mar 11 '13 at 15:48