How to fill case class from json with partial data?
Asked Answered
M

4

8
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?

Marieann answered 10/4, 2013 at 1:9 Comment(1)
Answer how to do this with play json library is also acceptable.Marieann
M
6

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")
 }
Meter answered 10/4, 2013 at 2:22 Comment(0)
S
5

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.

Saltarello answered 3/7, 2014 at 8:41 Comment(0)
P
4

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.

Prognosis answered 5/7, 2014 at 16:43 Comment(3)
No comment on the idea of "M" as default gender, by the way.Prognosis
It would be nice, if macro could generate whole read not just single default values, but for now it's best solution.Marieann
@codez: you could write a macro that would (that's what Play's Inception does, although it doesn't provide this functionality). The nice thing about this approach is that it's much more general—you can use default in this manner with any JSON library.Prognosis
D
0

Most scala JSON libraries choke on this.

We use an in-house JSON library that uses macros to provide sane defaults for missing fields. (also distinguishes between Null as none, and Undefined as default. So you could have an option with a default of Some if you wanted)

Hoping to opensource it soon.

EDIT: basically, i dont know of any others that do this. but its an ever-changing ecosystem, curious to see if anybody else has tackled it yet

Davison answered 2/7, 2014 at 22:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.