Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fill case class from json with partial data?

import net.liftweb.json._
import net.liftweb.json.JsonParser._

object test02 extends App {
    implicit val formats = DefaultFormats
    case class User(
        id: Int = 0,
        name: String = "John Doe",
        gender: String = "M")

    val s1=""" {"id":1,"name":"Bill","gender":"M"} """
    var r1=Serialization.read[User](s1)
    println(r1)

    val s2=""" {"id":1} """
    var r2=Serialization.read[User](s2)
    println(r2)  

}

Second Serialization.read causes exception: net.liftweb.json.MappingException: No usable value for name.

How could I possibly read data form json into case class, but if some fields are missing they are replaced with default values from case class?

like image 263
codez Avatar asked Apr 10 '13 01:04

codez


3 Answers

Looks like there's been an open ticket for this for quite a while: https://www.assembla.com/spaces/liftweb/tickets/534

In the meantime, one option is to use an Option:

    case class User(
        id: Int = 0,
        name: Option[String],
        gender: Option[String]) {
      def defaults = copy(
        name   = name   orElse Some("John Doe"),
        gender = gender orElse Some("M"))
    }
    // ...        
    val s2=""" {"id":1} """
    var r2=Serialization.read[User](s2)
    println(r2)  

That should give you:

 User(1,None,None)

And you could use something like this to fill-in default values:

 val r2 = Serialization.read[User](s2).defaults

 // r2: User = User(1,Some(John Doe),Some(M))

The other option is to use additional constructors for your case class:

 case class User(id: Int, name: String, gender: String)
 object User {
   def apply(id:Int): User = User(id, "John Doe", "M")
 }
like image 107
Faiz Avatar answered Nov 20 '22 05:11

Faiz


How to do it with the play json library, although, you have to provide the defaults in the parser and not only as default for the values:

case class User(id: Int, name: String, gender: String)

import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit val userReads: Reads[User] = (
  (__ \ "id").read[Int] and
  (__ \ "name").read[String].or(Reads.pure("John Doe")) and
  (__ \ "gender").read[String].or(Reads.pure("Male"))
)(User)

Json.fromJson[User](Json.parse("""{"id":1,"name":"Bill","gender":"M"}"""))

Json.fromJson[User](Json.parse("""{"id":1}"""))

I guess you could provide the defaults from the default parameter by creating a template instance of User and then passing the default from each field to Reads.pure instead of hardcoding a string there.

like image 5
johanandren Avatar answered Nov 20 '22 04:11

johanandren


Here's a Play solution that doesn't require you to specify the defaults twice or in some weird place—it uses a macro to find the appropriate default at compile-time.

First for the case class:

case class User(id: Int = 0, name: String = "John Doe", gender: String = "M")

Next you need to define DefaultFinder as I describe in this blog post. Then you're practically done:

import DefaultFinder._

import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit val userReads: Reads[User] = (
  (__ \ 'id).readNullable[Int] and
  (__ \ 'name).readNullable[String] and
  (__ \ 'gender).readNullable[String]
)((id, name, gender) => new User(
  id = if (id.isEmpty) default else id.get,
  name = if (name.isEmpty) default else name.get,
  gender = if (gender.isEmpty) default else gender.get
))

And finally:

scala> Json.fromJson[User](Json.parse("""{ "id": 1, "name": "Foo McBar" }"""))
res0: play.api.libs.json.JsResult[User] = JsSuccess(User(1,Foo McBar,M),)

scala> Json.fromJson[User](Json.parse("""{ "id": 2, "gender": "X" }"""))
res1: play.api.libs.json.JsResult[User] = JsSuccess(User(2,John Doe,X),)

Note that I'm not using getOrElse because the macro doesn't support it, but it could easily be made more general—it was just a quick proof-of-concept.

like image 4
Travis Brown Avatar answered Nov 20 '22 06:11

Travis Brown