2

I'm trying to represent the following JSON as a Scala case class:

     {
       "cars": {
          "THIS IS A DYNAMIC KEY 1": {
            "name": "bla 1",
          },
          "THIS IS A DYNAMIC KEY 2": {
            "name": "bla 2",
          }
          ...
      }

However, my JSON has dynamic keys that I won't know at runtime, and I'd like to use circe to encode/decode. I'm using the correct way to represent this using Scala?

import io.circe.generic.JsonCodec

@JsonCodec
case class Cars(cars: List[Car])

@JsonCodec
case class Car(whatShouldThisBe: CarDetails) // Not sure how to represent this?

@JsonCodec
case class CarDetails(name: String)
Rory
  • 798
  • 2
  • 12
  • 37
  • 1
    How "dynamic" is the key? One from a set/finite list of possibilities or "any valid string"? If the former, you can use `sealed trait` and several implemenations. If the latter, model `Car` as a tuple or `Cars` as a whole as a `Map`. – Mateusz Kubuszok Jan 08 '19 at 14:13
  • Thanks, the key can be any valid string – Rory Jan 08 '19 at 14:21

2 Answers2

6

I think you can just use a Map[String, CarDetails]. Your ADT then becomes:

import io.circe.generic.JsonCodec

@JsonCodec
case class Cars(cars: Map[String, CarDetails])

@JsonCodec
case class CarDetails(name: String)

The only tricky bit might be if you require there to be at least one CarDetails object, or if zero is acceptable. Circe does appear to support cats.data.NonEmptyMap should that be required.

Mark Kegel
  • 4,476
  • 3
  • 21
  • 21
3

The most straightforward way to handle a case like this would probably be to change the cars member of the Cars case class to have a type like Map[String, CarDetails], dropping the Car case class altogether. If you do that, your code will work exactly as-is (minus the Car definition), and will decode the JSON example you've provided.

If you want something closer to your case class structure, you could do the following:

import io.circe.Decoder
import io.circe.generic.JsonCodec

case class Cars(cars: List[Car])

object Cars {
  implicit val decodeCars: Decoder[Cars] =
    Decoder[Map[String, CarDetails]].prepare(_.downField("cars")).map(kvs =>
      Cars(
        kvs.map {
          case (k, v) => Car(k, v)
        }.toList
      )
    )
}

// I've added an `id` member here as a way to hold on to the JSON key.
case class Car(id: String, whatShouldThisBe: CarDetails)

@JsonCodec
case class CarDetails(name: String)

This will decode the same JSON but will include the dynamic keys in the Car level.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680