Skip to content
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

Merged
merged 12 commits into from
Oct 6, 2023
Merged

Conversation

kciesielski
Copy link
Member

@kciesielski kciesielski commented Oct 3, 2023

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:

  1. Enumerations are now understood as sealed hierarchies with case objects only, or native enum with parameterless cases. Both cases are encoded in the same way and have consistent schemas.
  2. Instead of sttp.tapir.generic.Configuration Pickler API now uses sttp.tapir.pickler.PicklerConfiguration. It allows to have specific defaults different than Configuration.default (we'd like to have discriminator = "$type", not None as a default).
  3. Coproduct schemas are consistent with PicklerConfiguration, which wasn't the case before.

@kciesielski kciesielski requested a review from adamw October 3, 2023 10:46
@kciesielski kciesielski marked this pull request as ready for review October 3, 2023 10:46
kciesielski added a commit that referenced this pull request Oct 3, 2023
@@ -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 {
Copy link
Member

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)
Copy link
Member

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 enums (parameterless and not)

Copy link
Member

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 :) )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have such tests

  1. Parameterless enums
  2. With parameters
    it should "support sealed hierarchies looking like enums" in {

Copy link
Member

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

Copy link
Member

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.)

@adamw
Copy link
Member

adamw commented Oct 3, 2023

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
@kciesielski kciesielski requested a review from adamw October 4, 2023 09:45
@@ -87,9 +87,10 @@ import sttp.tapir.generic.Configuration
given customConfiguration: Configuration = Configuration.default.withSnakeCaseMemberNames
```

## Enums / sealed traits / coproducts
## Sealed traits / coproducts
Copy link
Member

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 :)


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.
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

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":
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, much clearer now :)

@kciesielski kciesielski marked this pull request as draft October 5, 2023 13:17
@kciesielski
Copy link
Member Author

Converting back to draft, as I'm reworking quite a lot around coproducts and configuration as well.

@kciesielski kciesielski changed the title Fix handling for case-object enumerations Rework Pickler for coproducts and enums Oct 5, 2023
@kciesielski kciesielski marked this pull request as ready for review October 5, 2023 17:48
@adamw adamw merged commit 561c80f into master Oct 6, 2023
13 checks passed
@adamw
Copy link
Member

adamw commented Oct 6, 2023

Done! :)

@mergify mergify bot deleted the pickler-enum-case-objects branch October 6, 2023 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants