Update case class from incomplete JSON with Argonaut or Circe
Asked Answered
H

2

11

I need to create an updated instance from a case class instance (with any needed DecodeJsons implicitly derived), given an incomplete json (some fields missing). How can this be accomplished with Argonaut (preferably) or Circe (if I have to)?

Example:

case class Person(name:String, age:Int)
val person = Person("mr complete", 42)
val incompletePersonJson = """{"name":"mr updated"}"""
val updatedPerson = updateCaseClassFromIncompleteJson(person, incompletePersonJson)

println(updatedPerson)
//yields Person(mr updated, 42) 

I'm pretty sure I have to parse the json to json AST, then convert it to Shapeless LabelledGeneric, then use Shapeless update somehow to update the case class instance.


Edit 2

After reading the Shapeless source I found that I can generate my own "Default"-object. I managed to create a solution which requires the instance of the case class to be present while parsing the json. I was hoping to avoid this and instead provide the instance later. Anyway here it is:

import shapeless._
import argonaut._
import ArgonautShapeless._
import shapeless.ops.hlist.Mapper

case class Person(name: String, age: Int)

object MkDefault {

  object toSome extends Poly1 {
    implicit def default[P] = at[P](Some(_))
  }

  def apply[P, L <: HList, D <: HList]
  (p: P)
  (implicit
   g: Generic.Aux[P, L],
   mpr: Mapper.Aux[toSome.type, L, D]
  ): Default.Aux[P, mpr.Out] =
    Default.mkDefault[P, D](mpr(g.to(p)))
}


object Testy extends App {
    implicit val defs0 = MkDefault(Person("new name? NO", 42))
    implicit def pd = DecodeJson.of[Person]
    val i = """{"name":"Old Name Kept"}"""
    val pp = Parse.decodeOption[Person](i).get
    println(pp)
}

This yields Person(Old Name Kept,42).

Hitchcock answered 3/9, 2016 at 16:2 Comment(3)
Debugging ArgonautShapeless' DecodeJson inferring (ArgonautShapeless.derivedDecodeJson), i see that an object defaults=Defaults$AsOptions$$anon$9 is instantiated with values None :: None :: HNil. To me, it seems that if I could somehow replace this with an implicit instance I provide myself, I could make defaults to fill inn the missing json somehow.Hitchcock
The only thing I can think of that would do it in a type safe way is to re-serialize your existing case class to json, convert both to a Map[String, Any], then merge the maps, convert back to json, then re-parseFive
@Five that's actually not such a bad idea, I'll keep it in mind as a backup solution as it would obviously require more time and resources from the computer.Hitchcock
R
15

For the sake of completeness: support for "patching" instances like this has been provided in circe since the 0.2 release:

import io.circe.jawn.decode, io.circe.generic.auto._

case class Person(name: String, age: Int)

val person = Person("mr complete", 42)
val incompletePersonJson = """{"name":"mr updated"}"""

val update = decode[Person => Person](incompletePersonJson)

And then:

scala> println(update.map(_(person)))
Right(Person(mr updated,42))

My original blog post about this technique uses Argonaut (mostly since I wrote it a couple of months before I started working on circe), and that implementation is available as a library, although I've never published it anywhere.

Rearrange answered 22/9, 2016 at 12:42 Comment(2)
This is exactly what I was looking for. Switching from Argonaut to Circe in my projects now. Awesome work, keep it up :DHitchcock
Noticed a significant difference from Argonaut after going back from HTTP PATCH to HTTP POST testing with Circe: default values of case classes are completely ignored in Circe, whereas in Argonaut if a field was missing from the json and had a default value, it would decode ok. Also noticed an open issue on this in Circe: github.com/travisbrown/circe/issues/65 Wonder if it's scheduled for a release in the near future?Hitchcock
A
3

You can generate those implicit val defs / pd with macro annotation on Person (in object Person, for example, and do import Person._ to summon implicits). See this unfinished Simulacrum in scalameta (scala-reflect is fine too, but seems like scalameta can be enough here) for usage examples. Also you have to specify missing default value (42) somewhere, for example, in case class constructor (age: Int = 42, recognition can be done in macro too).

Adnah answered 17/9, 2016 at 19:28 Comment(2)
Thanks for this. I was looking to avoid creating my own macros as they usually devolve into maintainence-hell, but I havent looked at scalameta or Simulacrum yet. I will keep the bounty open out today in case someone posts a non-custom-macro shapeless solution for either circe or argonaut. If no replies comes in I'll just hand you the 50 for the effort :)Hitchcock
I'm working on more detailed guide to scalameta-based macro annotations. Hope to finish it in ~week. "simulacrum-meta" is just an example of scalameta's APIs. Generally speaking, scalameta should be preferred over scala-reflect, if it satisfies requirements (for example, semantic API is not supported yet). In this case I think scalameta should do the job.Adnah

© 2022 - 2024 — McMap. All rights reserved.