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