问题
Given the following example:
sealed trait Id
case class NewId(prefix: String, id: String) extends Id
case class RevisedId(prefix: String, id: String, rev: String) extends Id
case class User(key: Id, name: String)
val json = """
{
"key": {
"prefix": "user",
"id": "Rt01",
"rev": "0-1"
},
"name": "Bob Boberson"
}
"""
implicit val CodecUser: CodecJson[User] = casecodec2(User.apply, User.unapply)("key", "name")
implicit val CodecId: CodecJson[Id] = ???
json.decodeOption[User]
I need to write a CodecJson
for Id
that will decode an object when it has the proper structure.
Adding a discriminator field of some sort is a common suggestion for this, but I don't want to change the JSON I'm already producing/consuming with spray-json
and json4s
.
In those libraries your encoders/decoders are basically just PartialFunction[JValue, A]
and PartialFunction[A, JValue]
. If your value isn't defined in the domain it's a failure. It's a really simple, elegant solution I think. In addition to that you've got Extractors for the JSON types, so it's easy to match an object on fields/structure.
Rapture goes a step further, making field order unimportant and ignoring the presence of non-matching fields, so you could just do something like:
case json"""{ "prefix": $prefix, "id": $id, "rev": $rev }""" =>
RevisedId(prefix, id, rev)
That's really simple/powerful.
I'm having trouble figuring out how to do something similar with argonaut
. This is the best I've come up with so far:
val CodecNewId = casecodec2(NewId.apply, NewId.unapply)("prefix", "id")
val CodecRevisedId = casecodec3(RevisedId.apply, RevisedId.unapply)("prefix", "id", "rev")
implicit val CodecId: CodecJson[Id] =
CodecJson.derived[Id](
EncodeJson {
case id: NewId => CodecNewId(id)
case id: IdWithRev => RevisedId(id)
},
DecodeJson[Id](c => {
val q = RevisedId(c).map(a => a: Id)
q.result.fold(_ => CodecNewId(c).map(a => a: Id), _ => q)
})
)
So there's a few problems with that. I have to define extra codecs I don't intend to use. Instead of using the case-class extractors in the EncodeJson
for CodecJson[Id]
, I'm delegating to the other encoders I've defined. Just just doesn't feel very straight-forward or clean for classes that only have 2 or 3 fields.
The code for the DecodeJson
section is also pretty messy. Aside from an extra type-cast in the ifEmpty
side of the fold
it's identical to the code in DecodeJson.|||
.
Does anyone have a more idiomatic way to write a basic codec for Sum-types in argonaut that doesn't require a discriminator and can instead match on the structure of the json?
回答1:
This is the best I've been able to come up with. It doesn't have that fundamental feel of elegance that the partial functions do, but it is a good bit more terse and easier to decipher than my first attempt.
val CodecNewId = casecodec2(NewId.apply, NewId.unapply)("prefix", "id")
val CodecRevisedId = casecodec3(RevisedId.apply, RevisedId.unapply)("prefix", "id", "rev")
implicit val CodecId: CodecJson[Id] = CodecJson(
{
case id: NewId => CodecNewId(id)
case id: RevisedId => CodecRevisedId(id)
},
(CodecRevisedId ||| CodecNewId.map(a => a: Id))(_))
We're still using the "concrete" codecs for each sub-type. But we've gotten away from the CodecJson.derive
call, we don't need to wrap our encode function in an EncodeJson
, and we can map
our DecodeJson
function instead of type-casting so we can go back to using |||
instead of copying it's implementation, which makes the code a lot more readable.
This is definitely a usable solution, if not quite what I'd hoped for.
来源:https://stackoverflow.com/questions/39108841/decoding-a-sealed-trait-in-argonaut-based-on-json-structure