jsonscalashapelessargonautcirce

Update case class from incomplete JSON with Argonaut or Circe


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).


Solution

  • 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.