Circe asJson не кодирует свойства из абстрактного базового класса

Предположим, у меня есть следующий абстрактный базовый класс:

package Models

import reactivemongo.bson.BSONObjectID


abstract class RecordObject {
  val _id: String = BSONObjectID.generate().stringify
}

Который расширяется следующим классом конкретных случаев:

package Models

case class PersonRecord(name: String) extends RecordObject

Затем я пытаюсь получить строку JSON, используя следующий код:

import io.circe.syntax._
import io.circe.generic.auto._
import org.http4s.circe._
// ...

val person = new PersonRecord(name = "Bob")
println(person._id, person.name) // prints some UUID and "Bob"
println(person.asJso) // {"name": "Bob"} -- what happened to "_id"? 

Как видите, свойство _id: String, унаследованное от RecordObject, отсутствует. Я ожидаю, что встроенный кодировщик должен нормально работать для этого варианта использования. Мне действительно нужно построить свой собственный?


person foxtrotuniform6969    schedule 25.04.2020    source источник


Ответы (1)


Давайте посмотрим, что происходит при генерации энкодера. Circe использует shapeless для получения своих кодеков, поэтому достаточно проверить, во что разрешается shapeless, чтобы ответить на ваш вопрос. Итак, в аммоните:

@ abstract class RecordObject {
    val _id: String = java.util.UUID.randomUUID.toString
  }
defined class RecordObject

@ case class PersonRecord(name: String) extends RecordObject
defined class PersonRecord

@  import $ivy.`com.chuusai::shapeless:2.3.3`, shapeless._
import $ivy.$                             , shapeless._

@ Generic[PersonRecord]
res3: Generic[PersonRecord]{type Repr = String :: shapeless.HNil} = ammonite.$sess.cmd3$anon$macro$2$1@1123d461

Итак, String :: HNil. Достаточно справедливо - то, что делает shapeless, - это извлечение всех полей, доступных в конструкторе, преобразование одним способом и возвращение всех полей через конструктор при преобразовании другого.

В основном все производные классы типов работают таким образом, поэтому вы должны сделать возможным передачу _id в качестве конструктора:

abstract class RecordObject {
    val _id: String
}

case class PersonRecord(
  name: String,
  _id: String = BSONObjectID.generate().stringify
) extends RecordObject

Это помогло бы наследованию классов типов сделать свою работу. Если вы не можете изменить внешний вид PersonRecord... тогда да, вы должны написать свой собственный кодек. Хотя я сомневаюсь, что это будет легко, поскольку вы сделали _id неизменяемым и невозможным для установки извне через конструктор, поэтому его также будет сложно реализовать любым другим способом.

person Mateusz Kubuszok    schedule 25.04.2020
comment
Спасибо за отличный ответ! У меня было ощущение, что это связано с моим слабым пониманием наследования в Scala. Это действительно сработало, но теперь ответственность за реализацию генерации идентификаторов лежит на классах, унаследованных от RecordObject. Я бы предпочел, чтобы это было автоматически и чтобы идентификатор не изменялся после создания экземпляра класса. Есть ли способ сделать это? - person foxtrotuniform6969; 25.04.2020
comment
Вы должны определить конструктор, позволяющий переопределить _id — как еще вы можете воссоздать экземпляр с определенным идентификатором? Однако вы можете создать метод protected generateID в RecordObject и перегрузить конструктор для его использования: def this(name: String) = this(name, generateID). Однако у вас никогда не будет уверенности, что пользователь, расширяющий ваш интерфейс, будет его использовать. Что не имеет смысла ограничивать, поскольку вам все равно нужно передать произвольный String из внешнего мира, чтобы воссоздать значение. Эта строка может быть сломана в любой момент, прежде чем она достигнет конструктора. - person Mateusz Kubuszok; 25.04.2020
comment
Хм, я понимаю вашу точку зрения. Итак, что может быть хорошим способом создать некоторый тип объекта с автоматически сгенерированным свойством в качестве значения по умолчанию, которое другие объекты могут расширить, чтобы получить это свойство, без этих расширителей, выполняющих эту работу? Это вообще хорошая практика? - person foxtrotuniform6969; 25.04.2020
comment
Я бы просто создал отдельный тип для представления этого BSONObjectID. Этот тип будет иметь два общедоступных метода для создания самого себя, один из которых будет анализировать String во что-то вроде Either[Error, BSONObjectID], и один метод для создания нового идентификатора. Затем я бы вручную написал кодеки для этого идентификатора, чтобы убедиться, что, если идентификатор сломан, он не сможет разобрать Circe. В этот момент мне не нужно было бы опасаться, что нисходящие потоки могут использовать идентификаторы неправильным образом, поскольку у них не было бы возможности сделать это. - person Mateusz Kubuszok; 25.04.2020
comment
Попался. Большое спасибо за вашу помощь, ваше время и ваши быстрые ответы. Я приятно удивлен, что сообщество Scala на SO кажется более живым, чем даже всего год назад. - person foxtrotuniform6969; 25.04.2020
comment
Еще один дополнительный вопрос, что еще можно почитать по этой теме? Не только кодирование Circe, но и, возможно, погружение в наследование Scala trait/class/case class/abstract class в целом? - person foxtrotuniform6969; 25.04.2020
comment
Я предполагаю, что поиск таких вещей, как: вывод классов типов, создание незаменимых недопустимых состояний, АТД и, ну, типы и имплициты в целом, не повредит. - person Mateusz Kubuszok; 25.04.2020