json - 使用Shapeless HList轻松构建Json Decoder

标签 json scala shapeless

我正在尝试编写自己的小型轻量级玩具 Json 库,并且在试图想出一种简单的方法来指定编码器/解码器时遇到了障碍。我认为我有一个非常好的 dsl 语法,我只是不知道如何实现它。我认为使用 Shapeless HList 是可能的,但我以前从未使用过它,所以我对如何完成它一无所知。 我的想法是将这些 has 调用链接在一起,并构建某种 HList[(String, J: Mapper)] 链,然后如果可以的话有它在幕后尝试将 Json 转换为 HList[J] 吗? 这是实现的一部分,以及我想象的使用它的方式:

trait Mapper[J] {

  def encode(j: J): Json

  def decode(json: Json): Either[Json, J]

}

object Mapper {

  def strict[R]: IsStrict[R] =
    new IsStrict[R](true)

  def lenient[R]: IsStrict[R] =
    new IsStrict[R](false)

  class IsStrict[R](strict: Boolean) {

    def has[J: Mapper](at: String): Builder[R, J] =
      ???

  }

  class Builder[R, T](strict: Boolean, t: T) {

    def has[J: Mapper](at: String): Builder[R, J] =
      ???

    def is(decode: T => R)(encode: R => Json): Mapper[R] =
      ???

  }
}
Mapper
  .strict[Person]
  .has[String]("firstName")
  .has[String]("lastName")
  .has[Int]("age")
  .is {
    case firstName :: lastName :: age :: HNil =>
      new Person(firstName, lastName, age)
  } { person =>
    Json.Object(
      "firstName" := person.firstName,
      "lastName" := person.lastName,
      "age" := person.age
    )
  }

最佳答案

有一个很棒的资源可以学习如何使用 shapeless(HLIST 加 LabelledGeneric)来实现此目的:

戴夫·格内尔 (Dave Gurnell) 的《宇航员类型无形指南》

就您的情况而言,给定产品类型如下:

case class Person(firstName: String, lastName: String, age: Int)

编译器应该访问该类型实例的名称和值。书中详细描述了编译器如何在编译时创建 JSON 表示形式。

在您的示例中,您必须使用LabelledGeneric并尝试创建通用编码器/解码器。它是一个类型类,它将类型的表示形式创建为 HList,其中每个元素对应一个属性。

例如,如果您为 Person 类型创建 LabeledGeneric

val genPerson = LabelledGeneric[Person]

编译器推断出以下类型:

/* 
shapeless.LabelledGeneric[test.shapeless.Person]{type Repr = shapeless.::[String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("firstName")],String],shapeless.::[String with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("lastName")],String],shapeless.::[Int with shapeless.labelled.KeyTag[Symbol with shapeless.tag.Tagged[String("age")],Int],shapeless.HNil]]]}
*/

因此,名称和值已经使用 Scala 类型表示,现在编译器可以在编译时派生 JSON 编码器/解码器实例。下面的代码显示了创建您可以自定义的通用 JSON 编码器(本书第 5 章的摘要)的步骤。

第一步是创建 JSON 代数数据类型:

sealed trait JsonValue
case class JsonObject(fields: List[(String, JsonValue)]) extends JsonValue
case class JsonArray(items: List[JsonValue]) extends JsonValue
case class JsonString(value: String) extends JsonValue
case class JsonNumber(value: Double) extends JsonValue
case class JsonBoolean(value: Boolean) extends JsonValue
case object JsonNull extends JsonValue

所有这一切背后的想法是,编译器可以采用您的产品类型并使用 native 类型构建 JSON 编码器对象。

用于对类型进行编码的类型类:

trait JsonEncoder[A] {
   def encode(value: A): JsonValue
}

对于第一次检查,您可以创建 Person 类型所需的三个实例:

object Instances {

  implicit def StringEncoder : JsonEncoder[String] = new JsonEncoder[String] {
    override def encode(value: String): JsonValue = JsonString(value)
  }

  implicit def IntEncoder : JsonEncoder[Double] = new JsonEncoder[Double] {
    override def encode(value: Double): JsonValue = JsonNumber(value)
  }

  implicit def PersonEncoder(implicit strEncoder: JsonEncoder[String], numberEncoder: JsonEncoder[Double]) : JsonEncoder[Person] = new JsonEncoder[Person] {
    override def encode(value: Person): JsonValue =
      JsonObject("firstName" -> strEncoder.encode(value.firstName)
        :: ("lastName" -> strEncoder.encode(value.firstName))
        :: ("age" -> numberEncoder.encode(value.age) :: Nil))
  }
}

创建一个编码函数来注入(inject) JSON 编码器实例:

import Instances._

def encode[A](in: A)(implicit jsonEncoder: JsonEncoder[A]) = jsonEncoder.encode(in)

val person = Person("name", "lastName", 25)
println(encode(person))

给出:

 JsonObject(List((firstName,JsonString(name)), (lastName,JsonString(name)), (age,JsonNumber(25.0))))

显然,您需要为每个案例类创建实例。为了避免这种情况,您需要一个返回通用编码器的函数:

def createObjectEncoder[A](fn: A => JsonObject): JsonObjectEncoder[A] =
  new JsonObjectEncoder[A] {
    def encode(value: A): JsonObject =
      fn(value)
  }

它需要一个函数 A -> JsObject 作为参数。这背后的直觉是,编译器在遍历类型的 HList 表示形式时使用此函数来创建类型编码器,如 HList 编码器函数中所述。

然后,您必须创建 HList 编码器。这需要一个隐式函数来为 HNil 类型创建编码器,并为 HList 本身创建另一个编码器。

implicit val hnilEncoder: JsonObjectEncoder[HNil] =
    createObjectEncoder(hnil => JsonObject(Nil))

  /* hlist encoder */
implicit def hlistObjectEncoder[K <: Symbol, H, T <: HList](
    implicit witness: Witness.Aux[K],
    hEncoder: Lazy[JsonEncoder[H]],
    tEncoder: JsonObjectEncoder[T]): JsonObjectEncoder[FieldType[K, H] :: T] = {
    val fieldName: String = witness.value.name
    createObjectEncoder { hlist =>
      val head = hEncoder.value.encode(hlist.head)
      val tail = tEncoder.encode(hlist.tail)
      JsonObject((fieldName, head) :: tail.fields)
    }
  }

我们要做的最后一件事是创建一个隐式函数,为 Person 实例注入(inject) Encoder 实例。它利用编译器隐式解析来创建您类型的 LabeledGeneric 并创建编码器实例。

implicit def genericObjectEncoder[A, H](
     implicit generic: LabelledGeneric.Aux[A, H],
     hEncoder: Lazy[JsonObjectEncoder[H]]): JsonEncoder[A] =
     createObjectEncoder { value => hEncoder.value.encode(generic.to(value))
 }

您可以在 Instances 对象内编写所有这些定义。 导入实例._

val person2 = Person2("name", "lastName", 25)

println(JsonEncoder[Person2].encode(person2))

打印:

JsonObject(List((firstName,JsonString(name)), (lastName,JsonString(lastName)), (age,JsonNumber(25.0)))) 

请注意,您需要在 HList 编码器中包含 Symbol 的 Witness 实例。这允许在运行时访问属性名称。请记住,您的 Person 类型的 LabeledGeneric 类似于:

String with KeyTag[Symbol with Tagged["firstName"], String] ::
Int with KeyTag[Symbol with Tagged["lastName"], Int] ::
Double with KeyTag[Symbol with Tagged["age"], Double] ::

Lazy 类型需要为递归类型创建编码器:

case class Person2(firstName: String, lastName: String, age: Double, person: Person)

val person2 = Person2("name", "lastName", 25, person)

打印:

JsonObject(List((firstName,JsonString(name)), (lastName,JsonString(lastName)), (age,JsonNumber(25.0)), (person,JsonObject(List((firstName,JsonString(name)), (lastName,JsonString(name)), (age,JsonNumber(25.0)))))))

查看 Circe 或 Spray-Json 等库,了解它们如何使用 Shapeless 进行编解码器派生。

关于json - 使用Shapeless HList轻松构建Json Decoder,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62164024/

相关文章:

scala - 使用其他现有列添加新列 Spark/Scala

javascript - JSON 内的数组收到空

javascript - JQuery 将 json 解释为脚本?

scala - Scala 案例类的备用构造函数未定义 : not enough arguments for method

scala - HList 中的类型类包含元素?

scala - 子集和无形可扩展记录

json - 为基本特征具有(密封)类型成员的密封案例类族派生circe Codec

javascript - 如何在 Accordion react 原生中渲染复杂对象

javascript - 使用json数据在chart.js中绘制折线图

scala - 如何折叠 Scala 迭代器并获得延迟计算的序列作为结果?