Значения по умолчанию для отсутствующих свойств в форматах play 2 JSON

У меня есть эквивалент следующей модели в play scala:

case class Foo(id:Int,value:String)
object Foo{
  import play.api.libs.json.Json
  implicit val fooFormats = Json.format[Foo]
}

Для следующего экземпляра Foo

Foo(1, "foo")

Я бы получил следующий документ JSON:

{"id":1, "value": "foo"}

Этот JSON сохраняется и считывается из хранилища данных. Теперь мои требования изменились, и мне нужно добавить свойство в Foo. Свойство имеет значение по умолчанию:

case class Foo(id:String,value:String, status:String="pending")

Запись в JSON не проблема:

{"id":1, "value": "foo", "status":"pending"}

Однако чтение из него приводит к ошибке JsError из-за отсутствия пути «/status».

Как я могу обеспечить значение по умолчанию с наименьшим возможным шумом?

(ps: у меня есть ответ, который я опубликую ниже, но я не очень им доволен, и я бы проголосовал и принял любой лучший вариант)


person Jean    schedule 16.12.2013    source источник
comment
Привет Жан. Я копался в этой проблеме и нашел это. Кажется, нет никакой активности, кроме разговоров вокруг него. Вы уже нашли более элегантное решение, чем трансформер?   -  person Yar    schedule 27.11.2014
comment
Я до сих пор пользуюсь трансформерами. Пока у меня не будет достаточно времени, чтобы посмотреть, как писать свои собственные макросы, я думаю, что это все, что я смогу использовать :(   -  person Jean    schedule 27.11.2014


Ответы (7)


Воспроизвести 2.6+

Согласно @CanardMoussant answer, начиная с Play 2.6 макрос play-json был улучшен и предлагает множество новых функций, включая использование значения по умолчанию в качестве заполнителей при десериализации:

implicit def jsonFormat = Json.using[Json.WithDefaultValues].format[Foo]

Для игры ниже 2.6 лучшим вариантом остается использование одного из следующих вариантов:

play-json-extra

Я узнал о гораздо лучшем решении большинства недостатков, которые у меня были с play-json, включая тот, что указан в вопросе:

play-json-extra, который использует [play-json- extensions] внутри, чтобы решить конкретную проблему в этом вопросе.

Он включает макрос, который автоматически включает отсутствующие значения по умолчанию в сериализаторе/десериализаторе, делая рефакторинг гораздо менее подверженным ошибкам!

import play.json.extra.Jsonx
implicit def jsonFormat = Jsonx.formatCaseClass[Foo]

есть еще библиотека, которую вы можете проверить: play-json- дополнительно

преобразователи Json

Мое текущее решение состоит в том, чтобы создать JSON Transformer и объединить его с чтениями, сгенерированными макросом. Трансформатор создается следующим методом:

object JsonExtensions{
  def withDefault[A](key:String, default:A)(implicit writes:Writes[A]) = __.json.update((__ \ key).json.copyFrom((__ \ key).json.pick orElse Reads.pure(Json.toJson(default))))
}

Затем определение формата становится следующим:

implicit val fooformats: Format[Foo] = new Format[Foo]{
  import JsonExtensions._
  val base = Json.format[Foo]
  def reads(json: JsValue): JsResult[Foo] = base.compose(withDefault("status","bidon")).reads(json)
  def writes(o: Foo): JsValue = base.writes(o)
}

а также

Json.parse("""{"id":"1", "value":"foo"}""").validate[Foo]

действительно сгенерирует экземпляр Foo с примененным значением по умолчанию.

На мой взгляд, у него есть 2 существенных недостатка:

  • Имя ключа по умолчанию находится в строке и не будет выбрано при рефакторинге.
  • Значение по умолчанию дублируется, и если его изменить в одном месте, его нужно будет изменить вручную в другом.
person Jean    schedule 16.12.2013
comment
На данный момент я принял свой собственный ответ, а для вопросов с решениями, пытающимися ответить на первоначальное требование, я попытался объяснить, почему он не подходит. Если будет предложен лучший ответ, я соответствующим образом передвину принятый ответ. - person Jean; 03.07.2014
comment
В моем случае поле всегда было одинаковым (createdAt). В этом случае первый недостаток немного устранен: gist.github.com/felipehummel/5569dd69038142ce3bb5 - person Felipe Hummel; 23.07.2015
comment
Ссылка на play-json-extra не работает - person Thilo; 08.05.2017
comment
Действительно, я обновил ссылки на корень папки doc на github на данный момент github.com/aparo/play-json-extra/tree/master/doc - person Jean; 08.05.2017

Самый чистый подход, который я нашел, - это использовать "или чистый", например,

...      
((JsPath \ "notes").read[String] or Reads.pure("")) and
((JsPath \ "title").read[String] or Reads.pure("")) and
...

Это можно использовать обычным неявным способом, когда значением по умолчанию является константа. Когда он динамический, вам нужно написать метод для создания чтения, а затем ввести его в области видимости, а-ля

implicit val packageReader = makeJsonReads(jobId, url)
person Ed Staub    schedule 24.03.2015
comment
Пример кода подразумевает, что вы пишете свое сопоставление вручную, я предпочитаю способ улучшить сопоставление на основе макросов с правильными значениями по умолчанию, особенно для больших классов случаев. Помимо этого, мое собственное решение также использует Reads.pure в преобразователе json для установки значения по умолчанию. - person Jean; 24.03.2015
comment
@Jean - Да, я работаю с грязным json, где макрос Reads довольно бесполезен - неверные имена полей, требуется дополнительная обработка и т. д. Я могу видеть, где, если вы контролируете формат и качество JSON, используя его имело бы гораздо больше смысла. - person Ed Staub; 25.03.2015
comment
этот вариант использования довольно далек от того, что я описал в начальном вопросе :) - person Jean; 25.03.2015
comment
Это мне очень помогло. - person milos.ai; 19.11.2015

Альтернативное решение — использовать formatNullable[T] в сочетании с inmap из InvariantFunctor.

import play.api.libs.functional.syntax._
import play.api.libs.json._

implicit val fooFormats = 
  ((__ \ "id").format[Int] ~
   (__ \ "value").format[String] ~
   (__ \ "status").formatNullable[String].inmap[String](_.getOrElse("pending"), Some(_))
  )(Foo.apply, unlift(Foo.unapply))
person MikeMcKibben    schedule 22.07.2014
comment
Я выбрал этот подход; он работает, и он также чище, чем принятый в настоящее время ответ, но, к сожалению, он разделяет один из недостатков, заключающийся в том, что типы должны указываться в двух местах (класс case и определение чтения). - person Steven Bakhtiari; 29.09.2014
comment
Подход inmap интересен, мне нужно посмотреть, есть ли практический способ его использования, который не требует ручного написания всего сопоставления. В случае с Foo в этом вопросе все просто, но представьте, что у foo 15 атрибутов... - person Jean; 31.10.2014
comment
Согласен с @StevenBakhtiari - я использую этот подход, и он очень полезен, и не только для значений по умолчанию, но и для выполнения других операций со значением, возвращаемым из JsPath. - person DemetriKots; 15.07.2015

Я думаю, что официальный ответ теперь должен заключаться в использовании WithDefaultValues ​​​​в Play Json 2.6:

implicit def jsonFormat = Json.using[Json.WithDefaultValues].format[Foo]

Редактировать:

Важно отметить, что поведение отличается от библиотеки play-json-extra. Например, если у вас есть параметр DateTime со значением по умолчанию DateTime.Now, то теперь вы получите время запуска процесса - вероятно, не то, что вы хотите, - тогда как с play-json-extra у вас было время создания из JSON.

person CanardMoussant    schedule 03.07.2017
comment
Я также подтверждаю, что это работает в Play 2.6, нет необходимости добавлять дополнительные зависимости. Я попытался добавить play-json-extra, но у меня возникли некоторые проблемы, связанные с несовместимостью зависимостей в sbt. - person Robert Gabriel; 14.07.2017

Я только что столкнулся со случаем, когда я хотел, чтобы все поля JSON были необязательными (т.е. необязательными на стороне пользователя), но внутри я хочу, чтобы все поля были необязательными с точно определенными значениями по умолчанию на случай, если пользователь не указывает определенное поле. Это должно быть похоже на ваш вариант использования.

В настоящее время я рассматриваю подход, который просто оборачивает конструкцию Foo полностью необязательными аргументами:

case class Foo(id: Int, value: String, status: String)

object FooBuilder {
  def apply(id: Option[Int], value: Option[String], status: Option[String]) = Foo(
    id     getOrElse 0, 
    value  getOrElse "nothing", 
    status getOrElse "pending"
  )
  val fooReader: Reads[Foo] = (
    (__ \ "id").readNullable[Int] and
    (__ \ "value").readNullable[String] and
    (__ \ "status").readNullable[String]
  )(FooBuilder.apply _)
}

implicit val fooReader = FooBuilder.fooReader
val foo = Json.parse("""{"id": 1, "value": "foo"}""")
              .validate[Foo]
              .get // returns Foo(1, "foo", "pending")

К сожалению, это требует явного написания Reads[Foo] и Writes[Foo], чего, вероятно, вы хотели избежать? Еще один недостаток заключается в том, что значение по умолчанию будет использоваться только в том случае, если ключ отсутствует или значение равно null. Однако, если ключ содержит значение неправильного типа, вся проверка снова возвращает ValidationError.

Вложение таких необязательных структур JSON не является проблемой, например:

case class Bar(id1: Int, id2: Int)

object BarBuilder {
  def apply(id1: Option[Int], id2: Option[Int]) = Bar(
    id1     getOrElse 0, 
    id2     getOrElse 0 
  )
  val reader: Reads[Bar] = (
    (__ \ "id1").readNullable[Int] and
    (__ \ "id2").readNullable[Int]
  )(BarBuilder.apply _)
  val writer: Writes[Bar] = (
    (__ \ "id1").write[Int] and
    (__ \ "id2").write[Int]
  )(unlift(Bar.unapply))
}

case class Foo(id: Int, value: String, status: String, bar: Bar)

object FooBuilder {
  implicit val barReader = BarBuilder.reader
  implicit val barWriter = BarBuilder.writer
  def apply(id: Option[Int], value: Option[String], status: Option[String], bar: Option[Bar]) = Foo(
    id     getOrElse 0, 
    value  getOrElse "nothing", 
    status getOrElse "pending",
    bar    getOrElse BarBuilder.apply(None, None)
  )
  val reader: Reads[Foo] = (
    (__ \ "id").readNullable[Int] and
    (__ \ "value").readNullable[String] and
    (__ \ "status").readNullable[String] and
    (__ \ "bar").readNullable[Bar]
  )(FooBuilder.apply _)
  val writer: Writes[Foo] = (
    (__ \ "id").write[Int] and
    (__ \ "value").write[String] and
    (__ \ "status").write[String] and
    (__ \ "bar").write[Bar]
  )(unlift(Foo.unapply))
}
person bluenote10    schedule 19.03.2014

Вероятно, это не удовлетворит требованию «наименьшего возможного шума», но почему бы не ввести новый параметр как Option[String]?

case class Foo(id:String,value:String, status:Option[String] = Some("pending"))

При чтении Foo из старого клиента вы получите None, который я затем обработаю (с getOrElse) в вашем потребительском коде.

Или, если вам это не нравится, введите BackwardsCompatibleFoo:

case class BackwardsCompatibleFoo(id:String,value:String, status:Option[String] = "pending")
case class Foo(id:String,value:String, status: String = "pending")

а затем превратить его в Foo для дальнейшей работы, избегая необходимости иметь дело с такого рода гимнастикой данных все время в коде.

person Manuel Bernhardt    schedule 17.12.2013
comment
Проблема с параметром заключается в том, что затем я должен отображать или получать и обычно использовать монадические операции для доступа к значению, которое не является необязательным, просто установлено по умолчанию. Использование параметра там приведет к ложному сигналу при наборе текста, просто чтобы соответствовать текущей реализации библиотеки. - person Jean; 17.12.2013
comment
Верно, но вы упомянули, что ваши требования изменились, поэтому с точки зрения клиента (в данном случае хранилища данных) это значение является необязательным (это, так сказать, новая версия схемы). В этом случае я бы рекомендовал либо перенести данные в хранилище (установив значение по умолчанию везде, где оно отсутствует), либо иметь механизм, который обеспечивает изящную обратную совместимость с данными в хранилище данных. - person Manuel Bernhardt; 18.12.2013
comment
Точно: я пытаюсь создать механизм для изящной обработки обратной совместимости с данными в хранилище (или с клиентами, которые также не обновлялись), предоставляя приемлемое значение по умолчанию. таким образом, когда я читаю, а затем записываю свои данные обратно в хранилище, они автоматически обновляются, и моя система не взрывается для старых клиентов. Я, очевидно, использую Option, когда нет разумных значений по умолчанию, но во многих случаях я могу указать значение по умолчанию (как я делаю в классе case). - person Jean; 18.12.2013

Вы можете определить статус как вариант

case class Foo(id:String, value:String, status: Option[String])

используйте JsPath так:

(JsPath \ "gender").readNullable[String]
person Rubber Duck    schedule 02.01.2017
comment
За исключением того, что я не хочу иметь опцию, я хочу иметь значение, предоставляя значение по умолчанию, если оно не указано. Семантика Option[String] и String определенно не одинакова. - person Jean; 02.01.2017