Search code examples
scalacirce

How can I parse a JSON object with key-value objects into a List using Circe


Given a JSON string like this, how can I use Scala and Circe to parse the data into case classes:

val json = """{ "members": { "id1": { "name": "Foo" }, "id2": {"name": "bar" } } } """
case class Members(members: List[Member])
case class Member( id: String, name: String)

I have no control over the input JSON, but perhaps there is a way to use Circe to rewrite it to

{ "list": [ {"id": "id1", "name": "Foo"}, {"id": "id2", "name": "bar" } ] }

In which case the parsing should be possible with default decoders.

Note: the actual JSON has many more elements next to the name entry, in fact it is a nested structure (but there is no id element in there).

I have been able to parse it in the following way, but it involves tedious crafting of each decoder. I hope there is a more elegant way to achieve the goal?

import io.circe.Decoder
import io.circe.jawn.decode

val json = """{ "members": { "id1": { "name": "Foo" }, "id2": {"name": "bar" } } }"""
case class Member( id: String, name: String)
case class Members(members: List[Member])

implicit val MemberDecoder: Decoder[Member] = Decoder.instance { c =>
  val nameCursor = c.downField("name")
  val id = c.key.get
  
  for {
    name <- nameCursor.as[String]
  } yield Member(id, name)
}


implicit val MembersDecoder: Decoder[Members] =
  Decoder.instance { c =>
    val membersC = c.downField("members")
    val keys = membersC.keys.get
    val members = keys
      .map(k => {
        membersC.get[Member](k)
      }).toList
      .map(_.right.get)
    Right(Members( members))
  }

val result = decode[Members](json)

result //val res0: Either[io.circe.Error,Members] = Right(Members(List(Member(id1,Foo), Member(id2,bar))))

Solution

  • After researching and experimenting a bit more, I came up with this (still quite some code, but I guess the switch from a map of key->value to a list of (key + value) is not trivial.

    import io.circe.generic.semiauto.deriveDecoder
    import io.circe.jawn.decode
    import io.circe.{ACursor, Decoder, Json}
    
    val jsonMember = """{"id":"A", "name":"Aname"}"""
    val jsonMemberNoID = """{"name":"Aname"}"""
    val json =
      """{ "members": { "id1": { "name": "Foo" }, "id2": {"name": "bar" }, "id3": {"id":"ID3", "name":"baz"} } }"""
    case class Member(id: String, name: String)
    case class Members(members: List[Member])
    
    implicit val MemberDecoder: Decoder[Member] = deriveDecoder[Member].prepare { (aCursor: ACursor) => {
      aCursor.withFocus(json => {
        json.mapObject(jsonObject =>{
          if (!jsonObject.contains("id")){
            jsonObject.add("id", Json.fromString(aCursor.key.getOrElse("?")))
          } else {
            jsonObject
          }
        })
      })
    }}
    
    
    implicit val MembersDecoder: Decoder[Members] =
      Decoder.instance { c =>
        val membersC = c.downField("members")
        val keys = membersC.keys.get
        val members = keys
          .map(k => {
            membersC.get[Member](k)
          })
          .toList
          .map(_.toOption.get)
        Right(Members(members))
      }
    
    val memberResult = decode[Member](jsonMember)
    val result = decode[Members](json)
    val m2 = decode[Member](jsonMemberNoID)
    

    The last 3 lines produce this in a worksheet:

    val memberResult: Either[io.circe.Error,Member] = Right(Member(A,Aname))
    val result: Either[io.circe.Error,Members] = Right(Members(List(Member(id1,Foo), Member(id2,bar), Member(ID3,baz))))
    val m2: Either[io.circe.Error,Member] = Right(Member(?,Aname))