Search code examples
jsonscalajson4s

json4s parse json partially


I have a json model, where contents of certain attribute depend on the other attribute. Something like this:

"paymentMethod": "CREDIT_CARD",
"metaData": {
    "cardType": "VISA",
    "panPrefix": "",
    "panSuffix": "",
    "cardHolder": "",
    "expiryDate": ""
}

So when paymentMethod equals to CREDIT_CARD, the metadata object will contain attributes as described. In case of other payment method, there'll be different metadata.

I want to handle this situation in a future-proof way. What I'm trying to do is to not parse the metadata field right away, but keep it somehow "unparsed" until I've parsed the paymentMethod field. Then I'd take the metadata and applied appropriate parsing approach.

However I don't know which type to use for a Scala class field for such "late parsed" attributes. I've tried String, JsonInput, JObject, and they all are not suitable (either don't compile or can't be parsed). Any ideas which type can I use? Or, in other words:

case class CreditCardMetadata(
  cardType: String,
  panPrefix: String,
  panSuffix: String,
  cardHolder: String,
  expiryDate: String)

case class PaypalMetadata(...) // etc.

case class PaymentGatewayResponse(
  paymentMethod: String,
  metadata: ???)

Solution

  • 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": "foo@bar.com"
          },
          "otherField": "world"        
        }
        """)
    
    val cc =  json.extract[PaymentResponse]
    // PaymentResponse(CreditCardPayment(VISA,2015),hello)
    val pp =  json2.extract[PaymentResponse]
    // PaymentResponse(PayPalPayment(foo@bar.com),world)