-
Notifications
You must be signed in to change notification settings - Fork 422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework Pickler for coproducts and enums #3222
Conversation
@@ -523,6 +502,21 @@ class PicklerTest extends AnyFlatSpec with Matchers { | |||
codec.decode(encoded) shouldBe Value(inputObj) | |||
} | |||
|
|||
it should "handle sealed hierarchies consisting of objects only" in { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we should somehow separate tests per-feature, here: for enumerations (direct, wrapped, customised)
@@ -79,3 +79,14 @@ object Fixtures: | |||
} | |||
|
|||
case class StatusResponse(status: Status) | |||
|
|||
case class SealedVariantContainer(v: SealedVariant) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe also introduce a test case with enum
s (parameterless and not)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(unless we already don't have one :) )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have such tests
- Parameterless enums
it should "support simple enums" in { - With parameters
it should "support sealed hierarchies looking like enums" in {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, sorry, I didn't unwrap enough context :) But maybe if they will be in separate files it will be easier to see what's already there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(btw. open api docs tests are grouped in a similar way - each test has its own test classes in the companion object etc.)
it could also be good to explicitly mention in the documentation how we handle enumerations - how to customise, what's the default encoding (and when it is applied) |
- The entire enum codec building and customization logic is now in CreateDerivedEnumerationPickler
doc/endpoint/pickler.md
Outdated
@@ -87,9 +87,10 @@ import sttp.tapir.generic.Configuration | |||
given customConfiguration: Configuration = Configuration.default.withSnakeCaseMemberNames | |||
``` | |||
|
|||
## Enums / sealed traits / coproducts | |||
## Sealed traits / coproducts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can keep the enum
in the title, that's where I would look for info on how to serialize enums :)
doc/endpoint/pickler.md
Outdated
|
||
Pickler derivation for coproduct types (enums / sealed hierarchies) works automatically, by adding an `$type` discriminator field with the full class name. This is the default behavior of uPickle, but it can be overridden either by changing the discriminator field name, or by using custom logic to get field value from base trait. | ||
Pickler derivation for coproduct types (enums with parameters / sealed hierarchies) works automatically, by adding a `$type` discriminator field with the full class name. This is the default behavior of uPickle, but it can be overridden either by changing the discriminator field name, or by using custom logic to get field value from base trait. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm will that match the generated docs - that is, do we provide a pickler-specific Configuration
which matches this behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! This would almost certainly diverge in the schema, I'll add some tests and fixes accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adamw I'm implementing a fix for this and it raises a question: Should we really encode/decode default discriminator with full type name? This would make us compatible with uPickle default logic.
{"field": {"$type": "my.app.Green", "intensity": 100}}
However, the implementation of Configuration.default
is
implicit val default: Configuration = Configuration(Predef.identity, None, shortIdentitySubtypeTransformation)
so it expects shortIdentitySubtypeTransformation
, like Green
instead of my.app.Green
.
I think it's more important that we are consistent with passed configuration, and follow with Green
. If you agree, then here's another question: are we still consistent with passed configuration if its discriminator
is None and we generate a $type
field? I think we can treat this as OK, because None doesn't have to mean "no discriminator", and making it mean "use default" still makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that pickler has to provide its own configuration - setting the discriminator field to $type
. Question then is, how to sensibly provide such an implicit which has higher priority than the default one (should be easy, I think almost everything has higher priority than companion object implicits), but also in a way that's overridable by the user with a custom configuration. Can this be done at all?
As for whether to use short or long names - that's a good question ;) I think we should pick a sensible default, not necessarily follow current tapir's or upickle defaults. Full names seem to expose too much implementation details (the package name) to me, so I think short names are the better option. As for the discriminator field, $type
seems to be as good as any.
@@ -136,14 +137,23 @@ Schemas generated by picklers can be customized using annotations, just like wit | |||
|
|||
## Enumerations | |||
|
|||
Scala 3 `enums`, where all cases are parameterless, are treated as an enumeration (not as a coproduct / sealed hierarchy). They are also automatically handled by `Pickler.derived[T]`: enum values are encoded as simple strings representing the type name. For example: | |||
Tapir schemas and JSON codecs treats following cases as "enumerations": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
case _: EmptyTuple.type => Nil | ||
} | ||
|
||
/** Enumeration cases and case objects in an enumeration need special writers and readers, which are generated here, instead of being |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, much clearer now :)
Converting back to draft, as I'm reworking quite a lot around coproducts and configuration as well. |
Done! :) |
This PR's main intention was to fix handling for enumerations which are represented by sealed hierarchies. However, it has become a wider rework for enums and coproducts, which introduces quite a few changes and fixes:
enum
with parameterless cases. Both cases are encoded in the same way and have consistent schemas.sttp.tapir.generic.Configuration
Pickler API now usessttp.tapir.pickler.PicklerConfiguration
. It allows to have specific defaults different thanConfiguration.default
(we'd like to havediscriminator = "$type"
, notNone
as a default).PicklerConfiguration
, which wasn't the case before.