Search code examples
jsonscalaplay-json

Scala Play: List to Json-Array


I've got a List which holds some Personalization-objects. The latter is defined like this:

  sealed case class Personalization(firstname: String, lastname: String, keycardId: String)

I need to map this list to a Json-Array structure which has to look like this:

"personalization": [
    {
      "id": "firstname",
      "value": "John"
    },
    {
      "id": "lastname",
      "value": "Doe"
    }...

I am struggling with the part of mapping the field information to id/value pairs. Normally, I would create a play.api.libs.json.Format out of the Personalization class and let it map automatically -> Json.format[Personalization] - but this time, I need to create an array where an entry can hold n attributes.

Therefore I am asking for advice, if there is a possibility to use the Scala Play-Framework? Any input is much appreciated, thank you!


Solution

  • Writing as such JSON representation is not quite complex, using Writes.transform.

    import play.api.libs.json._
    
    case class Personalization(firstname: String, lastname: String, keycardId: String) // No need to seal a case class
    
    implicit def writes: Writes[Personalization] = {
      val tx: JsValue => JsValue = {
        case JsObject(fields) => Json.toJson(fields.map {
          case (k, v) => Json.obj("id" -> k, "value" -> v)
        })
    
        case jsUnexpected => jsUnexpected // doesn't happen with OWrites
      }
    
      Json.writes[Personalization].transform(tx)
    }
    

    Which can be tested as bellow.

    val personalization = Personalization(
      firstname = "First",
      lastname = "Last",
      keycardId = "Foo")
    
    val jsonRepr = Json.toJson(personalization)
    // => [{"id":"firstname","value":"First"},{"id":"lastname","value":"Last"},{"id":"keycardId","value":"Foo"}]
    

    Reading is a little bit tricky:

    implicit def reads: Reads[Personalization] = {
      type Field = (String, Json.JsValueWrapper)
    
      val fieldReads = Reads.seq(Reads[Field] { js =>
        for {
          id <- (js \ "id").validate[String]
          v <- (js \ "value").validate[JsValue]
        } yield id -> v
      })
    
      val underlying = Json.reads[Personalization]
    
      Reads[Personalization] { js =>
        js.validate(fieldReads).flatMap { fields =>
          Json.obj(fields: _*).validate(underlying)
        }
      }
    }
    

    Which can be tested as bellow.

    Json.parse("""[
      {"id":"firstname","value":"First"},
      {"id":"lastname","value":"Last"},
      {"id":"keycardId","value":"Foo"}
    ]""").validate[Personalization]
    // => JsSuccess(Personalization(First,Last,Foo),)
    

    Note that is approach can be used for any case class format.