Search code examples
jsonscalacirce

How to handle different JSON schemas and dispatch hem to be handled by the right parser?


I am currently building a very simple JSON parser in Scala that has to deal with two (slightly) different schemas. My objective is to parse one value in the json, and based on that value, I would like to dispatch it to the relevant decoder. I have used circe for my implementation, but other implementations and/or suggestions are also welcome.

I have formulated a simplified version for my example to help clarifying the question.

There are two types of JSONs that I can receive, either a stock:

"data": {
  "name": "XYZ"
},
  "type": "STOCK"
}

Or a quote (which is similar to the stock but includes a price).

"data": {
  "name": "ABC",
  "price": 1151.6214,
},
  "type": "QUOTE"
}

On my side, I have developed a simple decoder that looks like this (for the stock):

implicit private val dataDecoder: Decoder[Stock] = (hCursor: HCursor) => {
    for {
      isin <- hCursor.downField("data").downField("name").as[String]
      typ <- hCursor.downField("type").as[StockType]
    } yield Instrument(name, typ, LocalDateTime.now())
  }

I could also develop a parser that only parses the "type" part of the JSON, and then send the data to be handled by the relevant parser (quote or stock). However, I am wondering what is the:

  1. Efficient way to do this
  2. Idiomatic/clean way to do this

To help rephrase my question if needed, what is the right & efficient way to handle slightly different JSON schemas, and forward them to be handled by the right parser.


Solution

  • I usually encounter this situation when need to serialize ADTs. As somebody mentioned in the comments to your question, circe has support for ADT codec auto generation, however I usually prefer to manually write the codecs.

    In any case in a situation like your I would do something along these lines:

    sealed trait Data
    case class StockData(name: String) extends Data
    case class QuoteData(name: String, quote: Double) extends Data
    
    implicit val stockDataEncoder: Encoder[StockData] = ???
    implicit val stockDataDecoder: Decoder[StockData] = ???
    implicit val quoteDataEncoder: Encoder[QuoteData] = ???
    implicit val quoteDataDecoder: Decoder[QuoteData] = ???
    
    implicit val dataEncoder: Encoder[Data] = Encoder.instance {
      case s: StockData => stockDataEncoder(s).withObject(_.add("type", "stock))
      case q: QuoteData => quoteDataEncoder(q).withObject(_.add("type", "quote"))
    }
    
    implicit val dataDecoder: Decoder[Data] = Decoder.instance { c =>
      for {
        stype <- c.get[String]("type)
        res <- stype match {
           case "stock" => stockDataDecoder(c)
           case "quote" => quoteDataDecoder(c)
           case unk => Left(DecodingFailure(s"Unsupported data type: ${unk}", c.history))
        }
      } yield res
    }