2

I'm writing a Circe parser where the schema requires that at least one of two fields is set. This is quite specific and it doesn't seem to be a way to do it easily with Circe API.

Let's call our fields text and html.

I tried already to create a class, let's call it Content, add it to the general model as a single parameter and raise an exception in the constructor if both its fields (text and html) are None. The problem is how to define the decoder, because if I do something like this

implicit val decodeContent: Decoder[ItemContent] =
    Decoder.forProduct2("text", "html")(Content.apply)

it requires both fields to be present anyway.

What I would like would be to have a decoder that, if the field is missing, pass a None to the Content.apply but I don't think this is the expected behaviour.

Otherwise there should be a totally different solution but I cannot think of one.

Thank you

0__
  • 66,707
  • 21
  • 171
  • 266
Chobeat
  • 3,445
  • 6
  • 41
  • 59

1 Answers1

1

You can use Decoder#emap:

import io.circe._, parser._

case class ItemContent(text: Option[String], html: Option[String])

object ItemContent {
  implicit val decoder =
    Decoder.forProduct2("text", "html")(ItemContent.apply).emap {
      case ItemContent(None, None) => Left("Neither text nor html is present")
      case x                       => Right(x)
    }
}

assert {
  decode[ItemContent]("{}").isLeft &&
  decode[ItemContent]("""{"html": "foo"}""") == Right(
    ItemContent(None, Some("foo"))) &&
  decode[ItemContent]("""{"text": "bar"}""") == Right(
    ItemContent(Some("bar"), None)) &&
  decode[ItemContent]("""{"html": "foo", "text": "bar"}""") == Right(
    ItemContent(Some("bar"), Some("foo")))
}

Runnable version


To avoid specifying other fields it is possible to use semi-automatic derivation as a base:

import io.circe._, parser._, io.circe.generic.semiauto._

case class ItemContent(text: Option[String],
                       html: Option[String],
                       other: Int,
                       fields: String)

object ItemContent {
  implicit val decoder =
    deriveDecoder[ItemContent].emap { ic =>
      if (ic.text.isEmpty && ic.html.isEmpty)
        Left("Both `text` and `html` are missing")
      else Right(ic)
    }
}

Runnable version

Oleg Pyzhcov
  • 7,323
  • 1
  • 18
  • 30
  • uuuh, very interesting, I will try it. – Chobeat Aug 14 '17 at 19:07
  • the problem with this is that it's trying to parse something of the form { "content" : {"content_text":"abc"}} while I need to have "content_text" and "content_html" in the root, together with other fields. This way I'm treating it like a nested object while my goal is to parse just those two field this way and treat the others normally. The whole structure has many fields and I want to parse the others with the automatic derivation. Do you think it's possible? – Chobeat Aug 14 '17 at 19:57
  • @Chobeat you can start with derived decoder and add your validation on top of its result. I've updated the answer – Oleg Pyzhcov Aug 15 '17 at 06:56
  • It worked. Thank you very much. Your answers made me understand a lot more on how this library should be used. – Chobeat Aug 15 '17 at 09:35