Closed mrdziuban closed 6 months ago
@mrdziuban I think the approach looks great.
Determine how to handle cases where a type inherits from multiple sealed traits
In the current codebase, I suppose it doesn't matter, because the parent sealed trait
that a case class
inherits from does not change the serialization format. With this change, that is no longer true.
I think a reasonably first pass would be to just fail loudly if a case class
inherits from multiple sealed trait
s with different @key
s configured (or some configured and others not configured). The "different parent sealed trait
s have different discriminator fields" use case is probably obscure enough most people won't notice.
Another alternative would be to make the case class
have multiple duplicate discriminator fields if its parents have inconsistent @keys
, e.g. {"$type": "foo.Bar", "$type2": "foo.Baz", "data", "blah", ...}
. But I think that can be left to a follow PR, and for now just erroring out gives us the flexibility to adjust the design in future if we learn more
Consider if there's a more ergonomic way to get tagKey in taggedObjectContext
Could you describe what you think is un-ergonomic about the current approach? That would help me give a proper answer
fail loudly if a
case class
inherits from multiplesealed trait
s with different@key
s configured (or some configured and others not configured)
Good idea, updated to do so.
Could you describe what you think is un-ergonomic about the current approach?
I wasn't sure if adding def tagKey
to TaggedReader
was the best approach, since it proliferates to everywhere that constructs one, like Reader.merge
and Writer.merge
. That said, I tried some other approaches, like passing the tagKey
to findReader
, but ran into limitations where I didn't have the value available where findReader
was called.
Re: binary compatibility, I've made progress on it but am still having trouble with 5 problems:
* abstract method annotate(upickle.core.Types#Reader,java.lang.String,java.lang.String)upickle.core.Types#TaggedReader in interface upickle.core.Annotator is present only in current version
filter with: ProblemFilter.exclude[ReversedMissingMethodProblem]("upickle.core.Annotator.annotate")
* abstract method annotate(upickle.core.Types#ObjectWriter,java.lang.String,java.lang.String,upickle.core.Annotator#Checker)upickle.core.Types#TaggedWriter in interface upickle.core.Annotator is present only in current version
filter with: ProblemFilter.exclude[ReversedMissingMethodProblem]("upickle.core.Annotator.annotate")
* abstract method taggedWrite(upickle.core.Types#ObjectWriter,java.lang.String,java.lang.String,upickle.core.Visitor,java.lang.Object)java.lang.Object in interface upickle.core.Types is present only in current version
filter with: ProblemFilter.exclude[ReversedMissingMethodProblem]("upickle.core.Types.taggedWrite")
* private[..] abstract method tagKey()java.lang.String in interface upickle.core.Types#TaggedReader is present only in current version
filter with: ProblemFilter.exclude[ReversedMissingMethodProblem]("upickle.core.Types#TaggedReader.tagKey")
* abstract method findWriterWithKey(java.lang.Object)scala.Tuple3 in interface upickle.core.Types#TaggedWriter is present only in current version
filter with: ProblemFilter.exclude[ReversedMissingMethodProblem]("upickle.core.Types#TaggedWriter.findWriterWithKey")
I can fix the first two by implementing the annotate
methods in Types.scala
to construct the TaggedReader.Leaf
and TaggedWriter.Leaf
instances like they do in the LegacyApi
and AttributeTagged
in Api.scala
, but I'm not sure if there's any drawback to this and/or if they need to stay abstract in Types.scala
. These are the methods I'm talking about: https://github.com/com-lihaoyi/upickle/blob/d050f729197a96db7bf8fcde9a6d8645e70126c4/upickle/core/src/upickle/core/Types.scala#L287-L288
I'm stumped on the others. Do you have any idea how to handle them?
The abstracy method present only in current version
errors generally can be resolved by giving the method some default implementation. e.g.
if the new abstract method has an additional argument, make it instead a concrete method that forwards to the original abstract method.
If the new abstract method is a variant of a previousl6 existing abstract method, just make it return some reasonable default value
Thanks for the tips, all the changes are binary compatible now 🎉
I'll hopefully be able to add some tests over the next couple of days
Added a couple tests, I think this is ready for review now
Left one code comment. One more request, could you update the documentation site to include the new functionality? You can use sbt upickleReadme/run
to re-generate it in the upickleReadme/target/scalatex
folder
Also, could you update the PR description? A few lines summarizing what code changes are necessary, any alternatives you considered, and any other concerns you thought about, should be enough
could you update the documentation site to include the new functionality?
👍 done
could you update the PR description?
For sure, updated to add these details
Looks good to me. I kicked off CI and will merge it when green. Send me your bank details via email to haoyi.sg@gmail.com and i will close out the bounty
Whoops sorry about the test failures, fixing in a minute
Thanks!
Closes #578
Adds support for customizing the key used a discriminator for sealed hierarchies. The default is still
$type
, but can be customized by annotating thesealed trait
with@key("customKey")
.The code changes necessary to support this in
Writer
were:tagKey
parameter toTaggedWriter.Leaf
TaggedWriter#findWriterWithKey
method that includes thetagKey
in its return valueTaggedWriter#write0
to callfindWriterWithKey
and pass thetagKey
through totaggedWrite
AttributeTagged#taggedWrite
to use the giventagKey
in place of the old default keyThe code changes necessary to support this in
Reader
were:tagKey
method toTaggedReader
, which defaults to the old default keytagKey
parameter toTaggedReader.Leaf
,TaggedReader.Node
,TaggedReadWriter.Leaf
, andTaggedReadWriter.Node
to override the default value inTaggedReader
AttributeTagged#taggedObjectContext
to use the givenTaggedReader
'stagKey
in place of the old default keyI tried instead to pass the
tagKey
through tofindReader
, butfindReader
is called in a number of contexts where it wasn't possible to get thetagKey
value, for example inTaggedReader#visitString
and thevisitValue
method within theObjVisitor
returned byAttributeTagged#taggedObjectContext
.The one limitation of the customization is when a member of a sealed hierarchy inherits from multiple parents that would result in different discriminators, e.g. if two parents have
@key
annotations with different arguments, or if one parent has a@key
annotation and another doesn't. In these cases, the macros will fail with an error telling the user what went wrong and suggesting ways to fix it: