You can create a custom OFormat that will do this. By implicitly decorating JsPath with it you can include it in your json combinator definitions:
implicit class PathAdditions(path: JsPath) {
def readNullableIterable[A <: Iterable[_]](implicit reads: Reads[A]): Reads[A] =
Reads((json: JsValue) => path.applyTillLast(json).fold(
error => error,
result => result.fold(
invalid = (_) => reads.reads(JsArray()),
valid = {
case JsNull => reads.reads(JsArray())
case js => reads.reads(js).repath(path)
})
))
def writeNullableIterable[A <: Iterable[_]](implicit writes: Writes[A]): OWrites[A] =
OWrites[A]{ (a: A) =>
if (a.isEmpty) Json.obj()
else JsPath.createObj(path -> writes.writes(a))
}
/** When writing it ignores the property when the collection is empty,
* when reading undefined and empty jsarray becomes an empty collection */
def formatNullableIterable[A <: Iterable[_]](implicit format: Format[A]): OFormat[A] =
OFormat[A](r = readNullableIterable(format), w = writeNullableIterable(format))
}
This would allow you to create formats/reads/writes using the json combinator syntax like this:
case class Something(as: List[String], v: String)
import somewhere.PathAdditions
val reads: Reads[Something] = (
(__ \ "possiblyMissing").readNullableIterable[List[String]] and
(__ \ "somethingElse").read[String]
)(Something)
val writes: Writes[Something] = (
(__ \ "possiblyMissing").writeNullableIterable[List[String]] and
(__ \ "somethingElse").write[String]
)(unlift(Something.unapply))
val format: Format[Something] = (
(__ \ "possiblyMissing").formatNullableIterable[List[String]] and
(__ \ "somethingElse").format[String]
)(Something, unlift(Something.unapply))