如何将 JSON null 解码为空集合
How to decode a JSON null into an empty collection
假设我有一个像这样的 Scala 案例 class:
case class Stuff(id: String, values: List[String])
我希望能够将以下 JSON 值解码到其中:
{ "id": "foo", "values": ["a", "b", "c"] }
{ "id": "bar", "values": [] }
{ "id": "qux", "values": null }
在 Circe 中,您从泛型推导中获得的解码器适用于前两种情况,但不适用于第三种情况:
scala> decode[Stuff]("""{ "id": "foo", "values": ["a", "b", "c"] }""")
res0: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(a, b, c)))
scala> decode[Stuff]("""{ "id": "foo", "values": [] }""")
res1: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List()))
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res2: Either[io.circe.Error,Stuff] = Left(DecodingFailure(C[A], List(DownField(values))))
如何让我的解码器适用于这种情况,最好不必处理完全手写定义的样板文件。
使用游标进行预处理
解决这个问题最直接的方法是使用半自动推导并用prepare
预处理JSON输入。例如:
import io.circe.{Decoder, Json}, io.circe.generic.semiauto._, io.circe.jawn.decode
case class Stuff(id: String, values: List[String])
def nullToNil(value: Json): Json = if (value.isNull) Json.arr() else value
implicit val decodeStuff: Decoder[Stuff] = deriveDecoder[Stuff].prepare(
_.downField("values").withFocus(nullToNil).up
)
然后:
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res0: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List()))
它比简单地使用 deriveDecoder
稍微冗长一点,但它仍然可以让您避免写出所有 case class 成员的样板,如果您只有几个 case class 有需要这种待遇的会员,还不错。
处理缺失字段
如果您还想处理字段完全缺失的情况,则需要额外的步骤:
implicit val decodeStuff: Decoder[Stuff] = deriveDecoder[Stuff].prepare { c =>
val field = c.downField("values")
if (field.failed) {
c.withFocus(_.mapObject(_.add("values", Json.arr())))
} else field.withFocus(nullToNil).up
}
然后:
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res1: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List()))
scala> decode[Stuff]("""{ "id": "foo" }""")
res2: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List()))
这种方法本质上使您的解码器的行为方式与成员类型为 Option[List[String]]
.
时的行为方式完全相同
捆绑这个
您可以使用如下辅助方法使这更方便:
import io.circe.{ACursor, Decoder, Json}
import io.circe.generic.decoding.DerivedDecoder
def deriveCustomDecoder[A: DerivedDecoder](fieldsToFix: String*): Decoder[A] = {
val preparation = fieldsToFix.foldLeft[ACursor => ACursor](identity) {
case (acc, fieldName) =>
acc.andThen { c =>
val field = c.downField(fieldName)
if (field.failed) {
c.withFocus(_.mapObject(_.add(fieldName, Json.arr())))
} else field.withFocus(nullToNil).up
}
}
implicitly[DerivedDecoder[A]].prepare(preparation)
}
你可以这样使用:
case class Stuff(id: String, values: Seq[String], other: Seq[Boolean])
implicit val decodeStuff: Decoder[Stuff] = deriveCustomDecoder("values", "other")
然后:
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res1: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
scala> decode[Stuff]("""{ "id": "foo" }""")
res2: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
scala> decode[Stuff]("""{ "id": "foo", "other": [true] }""")
res3: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List(true)))
scala> decode[Stuff]("""{ "id": "foo", "other": null }""")
res4: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
这让您 95% 的回报都归功于半自动推导的易用性,但如果这还不够……
核选项
如果您有很多 class 成员需要这种处理,并且您不想修改它们,您可以采取更极端的方法来修改 Decoder
对于 Seq
无处不在:
import io.circe.Decoder
implicit def decodeSeq[A: Decoder]: Decoder[Seq[A]] =
Decoder.decodeOption(Decoder.decodeSeq[A]).map(_.toSeq.flatten)
那么如果你有这样的案例class:
case class Stuff(id: String, values: Seq[String], other: Seq[Boolean])
派生解码器将自动执行您想要的操作:
scala> import io.circe.generic.auto._, io.circe.jawn.decode
import io.circe.generic.auto._
import io.circe.jawn.decode
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res0: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
scala> decode[Stuff]("""{ "id": "foo" }""")
res1: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
scala> decode[Stuff]("""{ "id": "foo", "other": [true] }""")
res2: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List(true)))
scala> decode[Stuff]("""{ "id": "foo", "other": null }""")
res3: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
不过,我强烈建议坚持使用上面更明确的版本,因为依赖于为 Seq
更改 Decoder
的行为会使您处于必须非常注意 where 范围内的隐含内容。
这个问题经常出现,我们可能会在 Circe 的未来版本中为需要 null
映射到空集合的人提供具体支持。
您也可以在适用的情况下使用默认值:
@ConfiguredJsonCodec
case class Stuff(id: String, values: List[String]= Nil)
object Stuff {
implicit val configuration = Configuration.default.copy(useDefaults = true)
}
将很好地处理所有 3 种情况,以及一种缺失的字段。
免责声明:我完全知道我正在回答自己回答的 circe 的作者,我仍然认为添加这个非常简单的选项是一个很好的补充!
假设我有一个像这样的 Scala 案例 class:
case class Stuff(id: String, values: List[String])
我希望能够将以下 JSON 值解码到其中:
{ "id": "foo", "values": ["a", "b", "c"] }
{ "id": "bar", "values": [] }
{ "id": "qux", "values": null }
在 Circe 中,您从泛型推导中获得的解码器适用于前两种情况,但不适用于第三种情况:
scala> decode[Stuff]("""{ "id": "foo", "values": ["a", "b", "c"] }""")
res0: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(a, b, c)))
scala> decode[Stuff]("""{ "id": "foo", "values": [] }""")
res1: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List()))
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res2: Either[io.circe.Error,Stuff] = Left(DecodingFailure(C[A], List(DownField(values))))
如何让我的解码器适用于这种情况,最好不必处理完全手写定义的样板文件。
使用游标进行预处理
解决这个问题最直接的方法是使用半自动推导并用prepare
预处理JSON输入。例如:
import io.circe.{Decoder, Json}, io.circe.generic.semiauto._, io.circe.jawn.decode
case class Stuff(id: String, values: List[String])
def nullToNil(value: Json): Json = if (value.isNull) Json.arr() else value
implicit val decodeStuff: Decoder[Stuff] = deriveDecoder[Stuff].prepare(
_.downField("values").withFocus(nullToNil).up
)
然后:
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res0: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List()))
它比简单地使用 deriveDecoder
稍微冗长一点,但它仍然可以让您避免写出所有 case class 成员的样板,如果您只有几个 case class 有需要这种待遇的会员,还不错。
处理缺失字段
如果您还想处理字段完全缺失的情况,则需要额外的步骤:
implicit val decodeStuff: Decoder[Stuff] = deriveDecoder[Stuff].prepare { c =>
val field = c.downField("values")
if (field.failed) {
c.withFocus(_.mapObject(_.add("values", Json.arr())))
} else field.withFocus(nullToNil).up
}
然后:
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res1: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List()))
scala> decode[Stuff]("""{ "id": "foo" }""")
res2: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List()))
这种方法本质上使您的解码器的行为方式与成员类型为 Option[List[String]]
.
捆绑这个
您可以使用如下辅助方法使这更方便:
import io.circe.{ACursor, Decoder, Json}
import io.circe.generic.decoding.DerivedDecoder
def deriveCustomDecoder[A: DerivedDecoder](fieldsToFix: String*): Decoder[A] = {
val preparation = fieldsToFix.foldLeft[ACursor => ACursor](identity) {
case (acc, fieldName) =>
acc.andThen { c =>
val field = c.downField(fieldName)
if (field.failed) {
c.withFocus(_.mapObject(_.add(fieldName, Json.arr())))
} else field.withFocus(nullToNil).up
}
}
implicitly[DerivedDecoder[A]].prepare(preparation)
}
你可以这样使用:
case class Stuff(id: String, values: Seq[String], other: Seq[Boolean])
implicit val decodeStuff: Decoder[Stuff] = deriveCustomDecoder("values", "other")
然后:
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res1: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
scala> decode[Stuff]("""{ "id": "foo" }""")
res2: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
scala> decode[Stuff]("""{ "id": "foo", "other": [true] }""")
res3: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List(true)))
scala> decode[Stuff]("""{ "id": "foo", "other": null }""")
res4: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
这让您 95% 的回报都归功于半自动推导的易用性,但如果这还不够……
核选项
如果您有很多 class 成员需要这种处理,并且您不想修改它们,您可以采取更极端的方法来修改 Decoder
对于 Seq
无处不在:
import io.circe.Decoder
implicit def decodeSeq[A: Decoder]: Decoder[Seq[A]] =
Decoder.decodeOption(Decoder.decodeSeq[A]).map(_.toSeq.flatten)
那么如果你有这样的案例class:
case class Stuff(id: String, values: Seq[String], other: Seq[Boolean])
派生解码器将自动执行您想要的操作:
scala> import io.circe.generic.auto._, io.circe.jawn.decode
import io.circe.generic.auto._
import io.circe.jawn.decode
scala> decode[Stuff]("""{ "id": "foo", "values": null }""")
res0: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
scala> decode[Stuff]("""{ "id": "foo" }""")
res1: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
scala> decode[Stuff]("""{ "id": "foo", "other": [true] }""")
res2: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List(true)))
scala> decode[Stuff]("""{ "id": "foo", "other": null }""")
res3: Either[io.circe.Error,Stuff] = Right(Stuff(foo,List(),List()))
不过,我强烈建议坚持使用上面更明确的版本,因为依赖于为 Seq
更改 Decoder
的行为会使您处于必须非常注意 where 范围内的隐含内容。
这个问题经常出现,我们可能会在 Circe 的未来版本中为需要 null
映射到空集合的人提供具体支持。
您也可以在适用的情况下使用默认值:
@ConfiguredJsonCodec
case class Stuff(id: String, values: List[String]= Nil)
object Stuff {
implicit val configuration = Configuration.default.copy(useDefaults = true)
}
将很好地处理所有 3 种情况,以及一种缺失的字段。
免责声明:我完全知道我正在回答自己回答的 circe 的作者,我仍然认为添加这个非常简单的选项是一个很好的补充!