json4s частично разбирает json

У меня есть модель json, в которой содержимое определенного атрибута зависит от другого атрибута. Что-то вроде этого:

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

Поэтому, когда paymentMethod равно CREDIT_CARD, объект metadata будет содержать описанные атрибуты. В случае другого способа оплаты будут другие метаданные.

Я хочу справиться с этой ситуацией с расчетом на будущее. Что я пытаюсь сделать, так это не анализировать поле metadata сразу, а оставить его каким-то образом «неразобранным», пока я не проанализирую поле paymentMethod. Затем я взял метаданные и применил соответствующий подход к синтаксическому анализу.

Однако я не знаю, какой тип использовать для поля класса Scala для таких атрибутов с «поздним анализом». Я пробовал String, JsonInput, JObject, и все они не подходят (либо не компилируются, либо не разбираются). Любые идеи, какой тип я могу использовать? Или, другими словами:

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

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

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

person Haspemulator    schedule 13.07.2015    source источник


Ответы (3)


Вы можете создать CustomSerializer для прямого анализа метаданных. Что-то вроде :

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
))

Что можно использовать как:

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": "[email protected]"
      },
      "otherField": "world"        
    }
    """)

val cc =  json.extract[PaymentResponse]
// PaymentResponse(CreditCardPayment(VISA,2015),hello)
val pp =  json2.extract[PaymentResponse]
// PaymentResponse(PayPalPayment([email protected]),world)
person Peter Neyens    schedule 13.07.2015
comment
Эй, спасибо, ваш ответ поставил меня на правильный путь. Мне нужно было только какое-то специальное решение, поэтому я опубликовал свой собственный ответ, основанный на вашем, чтобы проиллюстрировать, что это можно реализовать с меньшим количеством кода. Для других, читающих это, этот ответ — правильный путь, но если вам нужно что-то быстрое, мой ответ тоже сработает. - person Haspemulator; 10.08.2015

Вы можете использовать Map[String, String]. В нем будет все, что вам может понадобиться.

person Carlos Vilchez    schedule 13.07.2015

Ответ Питера Нейенса вдохновил меня на реализацию собственного решения. Это не так универсально, как у него, но в моем случае мне нужно было что-то действительно простое и специальное. Вот что я сделал:

Можно определить класс case с полем неизвестного типа, представленным типом JObject. Что-то вроде этого:

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

Когда такой json анализируется в такой класс case, это поле не анализируется сразу, а содержит всю необходимую информацию. Затем можно разобрать его на отдельном шаге:

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

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