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?
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