Вывод "null" для опции [T] в сериализации play-json, когда значение равно None

Я использую макросы play-json для определения неявного Writes для сериализации JSON. Однако по-видимому, по умолчанию play-json пропускает поля, для которых поля Option установлены на None. Есть ли способ изменить значение по умолчанию, чтобы оно выводило null вместо этого? Я знаю, что это возможно, если я определяю собственное определение Writes, но я заинтересован в том, чтобы делать это с помощью макросов, чтобы уменьшить шаблонный код.

Пример

case class Person(name: String, address: Option[String])

implicit val personWrites = Json.writes[Person]    
Json.toJson(Person("John Smith", None))

// Outputs: {"name":"John Smith"}
// Instead want to output: {"name":"John Smith", "address": null}

Ответ 1

Макрос Json.writes генерирует writeNullable[T] для дополнительных полей. Как вы знаете (или нет), writeNullable[T] опускает поле, если значение равно None, тогда как write[Option[T]] генерирует нулевое поле.

Определение пользовательского сценария - единственный вариант, который вы должны получить.

( 
  (__ \ 'name).write[String] and
  (__ \ 'address).write[Option[String]]
)(unlift(Person.unapply _))

Ответ 2

Не настоящее решение для вас. Но немного лучше, чем вручную писать записи

Я создал вспомогательный класс, который может "обеспечивать" поля.

implicit class WritesOps[A](val self: Writes[A]) extends AnyVal {
    def ensureField(fieldName: String, path: JsPath = __, value: JsValue = JsNull): Writes[A] = {
      val update = path.json.update(
        __.read[JsObject].map( o => if(o.keys.contains(fieldName)) o else o ++ Json.obj(fieldName -> value))
      )
      self.transform(js => js.validate(update) match {
        case JsSuccess(v,_) => v
        case err: JsError => throw new JsResultException(err.errors)
      })
    }

    def ensureFields(fieldNames: String*)(value: JsValue = JsNull, path: JsPath = __): Writes[A] =
      fieldNames.foldLeft(self)((w, fn) => w.ensureField(fn, path, value))

}

чтобы вы могли писать

Json.writes[Person].ensureFields("address")()

Ответ 3

Аналогичный ответ выше, но еще один синтаксис для этого:

implicit val personWrites = new Writes[Person] {
  override def writes(p: Person) = Json.obj(
    "name" -> p.name,
    "address" -> p.address,
  )
}

Ответ 4

Это просто:

implicit val personWrites = new Writes[Person] {
  override def writes(p: Person) = Json.obj(
    "name" -> p.name,
    "address" -> noneToString(p.address),
  )
}

def optToString[T](opt: Option[T]) = 
   if (opt.isDefined) opt.get.toString else "null"

Ответ 5

Вы можете определить что-то вроде этого:

  implicit class JsPathExtended(path: JsPath) {
    def writeJsonOption[T](implicit w: Writes[T]): OWrites[Option[T]] = OWrites[Option[T]] { option =>
      option.map(value =>
        JsPath.createObj(path -> w.writes(value))
      ).getOrElse(JsPath.createObj(path -> JsNull))
    }
  }

И если вы используете игровые рамки:

  implicit val totoWrites: Writes[Toto] = (
      (JsPath \ "titre").write[String] and
        (JsPath \ "option").writeJsonOption[String] and
        (JsPath \ "descriptionPoste").writeNullable[String]
      ) (unlift(Toto.unapply))

   implicit val totoReads: Reads[Toto] = (
      (JsPath \ "titre").read[String] and
        (JsPath \ "option").readNullable[String] and
        (JsPath \ "descriptionPoste").readNullable[String]
      ) (Toto.apply _)

Ответ 6

Вы можете свернуть свой вариант и переопределить поведение сериализации:

case class MyOption[T](o: Option[T])
implicit def myOptWrites[T: Writes] = Writes[MyOption[T]](_.o.map(Json.toJson).getOrElse(JsNull))

ПРЕДОСТЕРЕЖЕНИЯ:

1) этот подход хорош только для классов случаев, используемых исключительно как определение протокола сериализации. Если вы повторно используете некоторые классы модели данных в контроллерах - определите пользовательские сериализаторы, чтобы они не загрязняли модель.

2) (относится только к Option), если вы используете один и тот же класс для записи и чтения. Для воспроизведения потребуется присутствие обернутых полей (возможно, нулевых) во время десериализации.

PS: не удалось сделать то же самое с маркировкой типа. Ошибка компилятора похожа на No instance of play.api.libs.json.Writes is available for tag.<refinement> (учитывая, что необходимые записи были определены явно). Похоже, ошибка воспроизведения макроса.

Ответ 7

Вы можете использовать пользовательскую неявную JsonConfiguration, см. Настройка макроса для вывода null

implicit val config = JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)

implicit val personWrites = Json.writes[Person]    
Json.toJson(Person("John Smith", None))