Home > Blockchain >  How do I throw an error when an unknown field is present whilst reading JSON with Scala Play?
How do I throw an error when an unknown field is present whilst reading JSON with Scala Play?

Time:11-20

With JSON schemas, if you want the schema to fail validation if have any additional fields you can just throw an "additionalProperties": false on the schema and call it a day a bit like this:

{
    "$schema": "http://json-schema.org/draft-07/schema",
    "type": "object",
    "title": "",
    "description": "",
    "properties": {
        "fieldOne": {
            "type": "string",
            "description": "Example String"
        }
    },
    "additionalProperties": false
}

However, if I have the following case class/object:

case class MyThing(fieldOne: Option[String])

object MyThing {
  implicit val reads: Reads[MyThing] = Json.reads[MyThing]
}

and provide it stuff other than fieldOne, it'll still read the JSON in as a case class correctly but the case class would be empty.

Is there a way to error when additional fields are provided in JSON when reading from JSON to a case class?

CodePudding user response:

Play JSON doesn't natively have this but in a custom Reads you have access to the JsValue/JsObject from the simple parse. So for something simple, you could do something like:

object MyThing {
  // Single-abstract method should work, if not more explicitly extend Reads
  implicit val reads: Reads[MyThing] = { json: JsValue =>
    json match {
      case JsObject(kv) =>
        val keys = kv.keySet
        if (keys != expectedFields) {
          (keys -- expected).headOption.map { unexpected =>
            JsError(s"Encountered unexpected field $unexpected")
          }.getOrElse(JsError("Must be a non-empty object"))
        } else derivedReads.reads(json)
      case _ => JsError(JsonValidationError("must be an object"))
    }
  }

  private val expectedFields = Set("fieldOne")
  private val derivedReads = Json.reads[MyThing]
}

In general, assuming you have a corresponding Writes which obeys a round-trip property you can do something like:

def strictify[T](reads: Reads[T], writes: Writes[T]): Reads[T] = new Reads[T] {
  def reads(json: JsValue): JsResult =
    reads.reads(json).filter { t =>
      val writeback = writes.writes(t)
      writeback == json
    }
}

This is strictly less-efficient than checking in a custom Reads, but it does enable

object MyThing {
  implicit val writes: Writes[MyThing] = Json.writes[MyThing]
  implicit val reads: Reads[MyThing] = strictify(Json.reads[MyThing], writes)
}

which would probably win if clarity is more important than performance.

  • Related