Подтвердить что ты не робот

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

У меня есть эквивалент следующей модели в игре 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: У меня есть ответ, который я опубликую ниже, но я не очень доволен им и буду продвигать и принимать любой лучший вариант)

4b9b3361

Ответ 1

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

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

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

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

игра-JSON-экстра

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

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

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

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

в библиотеке вы можете больше проверить: play-json-extra

Трансформаторы Json

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

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 с применяемым значением по умолчанию.

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

  • Имя ключа defaulter находится в строке и не будет подхвачено рефакторингом
  • Значение по умолчанию дублируется и если оно изменено в одном месте, необходимо будет изменить вручную на другом

Ответ 2

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

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

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

implicit val packageReader = makeJsonReads(jobId, url)

Ответ 3

Альтернативным решением является использование 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))

Ответ 4

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

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

Edit:

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

Ответ 5

Я просто столкнулся с тем случаем, когда я хотел, чтобы все поля 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. Однако, если ключ содержит значение неправильного типа, то снова вся проверка возвращает a 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))
}

Ответ 6

Это, вероятно, не удовлетворяет требованию "наименее возможного шума", но почему бы не ввести новый параметр как 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, чтобы работать с ним дальше, избегая при этом иметь дело с подобным типом данных в программе.

Ответ 7

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

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

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

(JsPath \ "gender").readNullable[String]