Search code examples
jsonscalaplayframeworkplay-json

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


With JSON schemas, if you want the schema to fail validation if it finds 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?


Solution

  • 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.