1

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))))
3
  • scalaVersion := "2.13.7" val circeVersion = "0.14.1" Commented Dec 8, 2021 at 17:17
  • No, a custom decoder is the way to go. Commented Dec 8, 2021 at 18:24
  • 1
    In the end I decided to change the case classes to closer match the JSON. Using members: Map[String, Member] turns out to be more practical than the members: List[Member] Commented Dec 15, 2021 at 18:58

2 Answers 2

1

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))
Sign up to request clarification or add additional context in comments.

Comments

0

You shouldn't need to write a custom Member decoder. You can just modify the json before decoding it. Try something like

(membersC.get(k) :+ ("id", k)).as[Member]

that might not be exactly the right syntax (I'm not super familiar with circe) but hopefully you get the idea.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.