Accepted answer

You could create a CustomSerializer to parse the metadata directly. Something like :

case class PaymentResponse(payment: Payment, otherField: String)

sealed trait Payment
case class CreditCardPayment(cardType: String, expiryDate: String) extends Payment
case class PayPalPayment(email: String) extends Payment

object PaymentResponseSerializer extends CustomSerializer[PaymentResponse]( format => ( 
    case JObject(List(
           JField("paymentMethod", JString(method)),
           JField("metaData", metadata),
           JField("otherField", JString(otherField))
         )) =>
      implicit val formats = DefaultFormats
      val payment = method match {
        case "CREDIT_CARD" => metadata.extract[CreditCardPayment]
        case "PAYPAL" => metadata.extract[PayPalPayment]
      PaymentResponse(payment, otherField)
  { case _ => throw new UnsupportedOperationException } // no serialization to json

Which can be used as:

implicit val formats = DefaultFormats + PaymentResponseSerializer

val json = parse("""
        "paymentMethod": "CREDIT_CARD",
        "metaData": {
            "cardType": "VISA",
            "expiryDate": "2015"
        "otherField": "hello"

val json2 = parse("""
      "paymentMethod": "PAYPAL",
      "metaData": {
          "email": ""
      "otherField": "world"        

val cc =  json.extract[PaymentResponse]
// PaymentResponse(CreditCardPayment(VISA,2015),hello)
val pp =  json2.extract[PaymentResponse]
// PaymentResponse(PayPalPayment(,world)


You can use a Map[String, String]. It will contain anything you may need.


The answer by Peter Neyens has inspired me to implement my own solution. It's not as generic as his, but in my case I needed something really simple and ad-hoc. Here's what I've done:

It's possible to define a case class with the field of unknown type is represented by a JObject type. Something like this:

case class PaymentGatewayResponse(
  default: Boolean,
  paymentMethod: String,
  visibleForCustomer: Boolean,
  active: Boolean,
  metaData: JObject)

When such json is parsed into such case class, this field is not parsed immediately, but contains all the necessary information. Then it's possible parse it in a separate step:

case class CreditCardMetadata(
  cardType: String,
  cardObfuscatedNumber: String,      
  cardHolder: String,
  expiryDate: String)

val response: PaymentGatewayResponse = doRequest(...) { r =>
      r.paymentMethod match {
        case "CREDIT_CARD" => r.metaData.extract[CreditCardMetadata]
        case unsupportedType: String => throw new UnsupportedPaymentMethodException(unsupportedType)

Related Query

More Query from same tag