0
votes

In a PUT request from a client device, I would like the application/json body to look like {"content": []} in order to clear a list I'm maintaining for the client. However, in a Play Form such key-value pair where the value is an empty array disappears after form parsing.

You can see that in regular (non-Form) Play JSON parsing, the empty array is maintained as expected.

[info] Starting scala interpreter...
Welcome to Scala 2.11.12 (OpenJDK 64-Bit Server VM, Java 1.8.0_212).
Type in expressions for evaluation. Or try :help.

scala> import play.api.libs.json._
import play.api.libs.json._

scala> val json = Json.parse("""{"content": []}""")
json: play.api.libs.json.JsValue = {"content":[]}

However, in Play's form parsing, the behavior is different.

private[data] object FormUtils {
  def fromJson(prefix: String = "", js: JsValue): Map[String, String] = js match {
    case JsObject(fields) =>
      val prefix2 = Option(prefix).filterNot(_.isEmpty).map(_ + ".").getOrElse("")
      fields.iterator
        .map { case (key, value) => fromJson(prefix2 + key, value) }
        .foldLeft(Map.empty[String, String])(_ ++ _)
    case JsArray(values) =>
      values.zipWithIndex.iterator
        .map { case (value, i) => fromJson(s"$prefix[$i]", value) }
        .foldLeft(Map.empty[String, String])(_ ++ _)
    case JsNull           => Map.empty
    case JsUndefined()    => Map.empty
    case JsBoolean(value) => Map(prefix -> value.toString)
    case JsNumber(value)  => Map(prefix -> value.toString)
    case JsString(value)  => Map(prefix -> value.toString)
  }
}

https://github.com/playframework/playframework/blob/826c76ee967d8ec35b76b9a8b594bfaa676a9510/core/play/src/main/scala/play/api/data/Form.scala#L397

The general idea of the function is a recursive traversal of the incoming JSValue objects and arrays where null, undefined, booleans, numbers, strings, and empty objects and arrays serve as base cases.

In the JsArray case, if values is empty then it returns Map.empty[String, String]. As a result, both [] and {"content": []} will be parsed to Map.empty[String, String]. For a form, the [] case seems reasonable, but {"content": []} is a legitimate key-value pair that should be kept in the form. Note that the same issue occurs with an empty JsObject.

Does this seem like a reasonable issue? If so, any workarounds that you can think of? I understand that it could be difficult for Play to change this since users of the framework might already be depending on this behavior.

1

1 Answers

0
votes

I think that is correct behavior, since as far as forms are concerned there is no concept of an Array.

A form simply consists of a set of key-value-pairs and an array is just flattened to fit into this scheme, a la path.to.array[0] = value.

The form, however, can then be mapped to a model which actually has a sequence of values.