Skip to content

Conversation

@pdvrieze
Copy link
Contributor

This is an implementation for #2199 and #1865. This implementation just modifies the subclass method in the builder. It does (internally) expose the registered children in SealedSerializer. It is not possible to trigger this behaviour from manually written serializers (instead of SealedSerializer).

This does also remove the prohibition of registering sealed classes, but note that it will register the children of the sealed class, but not the sealed parent itself. The code does allow for overlaps (by ignoring the registration in this case) to accommodate overlapping hierarchies (although this can be abused in some ways) - it does mean that sealed child serializers can be manually overridden by registering the sub-serializer before the sealed parent.

@pdvrieze pdvrieze changed the title Sealed modules Implementation for automatically registering sealed serializers in polymorphic modules Feb 20, 2023
@sandwwraith
Copy link
Member

Hi, thanks for the PR! Feature is indeed looks useful, although I'm a reluctant about changing the behavior of existing function (it didn't throw exception on sealed classes before, so it's not adding a completely new functionality). Have you though about other possibilities of doing this? Like adding new subclassesOf function. Also, this doesn't quite fix #1865 because IIRC there was explicit request of creating new SealedClassSerializer out of existing two. @Whathecode will such function solve your problems?

@pdvrieze
Copy link
Contributor Author

I'll update the pull request later when I have time. While this introduces "changed" behaviour the current (without this request) implementation is not actually valid.

As sealed classes (and interfaces) are abstract they don't actually make valid serializers in the polymorphic case.

For serialization the value with never be directly of the sealed type, for deserialization it will never create a sealed instance.

The only challenge with changing existing behaviour is when existing code does register the base for polymorphic serialization, this could have 2 consequences:

  • The children may already be registered, this will cause duplicate registration exceptions
  • If the children weren't registered they would now be, perhaps making certain code paths work accidentally.

As the current code doesn't (appear to) throw an exception in case sealed parent is registered it may be best to use a separate function name. I'll update this in the pull request.

@Whathecode
Copy link
Contributor

Whathecode commented Apr 29, 2023

@sandwwraith At a glance, no. #1865 is a different use case.

At runtime (this is a generic framework), I need to be able to create a serializer which can handle all subclasses of multiple sealed classes that extend from this common interface.

I don't have the types available at compile time, thus I can't register them in a serial module as you would 'normally' do.

My use case is a framework in which the app building on top of it links in additional types, through reflection, which extend from an interface defined in the framework. The framework then supports serializing aggregations of such extending classes.

I haven't looked at whether I could somehow create a SerializersModule with dynamically retrieved types through reflection, which perhaps is possible? I would then need to expose that module instead of a serializer to the caller. If that's possible, I guess this feature could be used to achieve something similar? But, I currently don't have time to look into this.

@pdvrieze
Copy link
Contributor Author

pdvrieze commented May 2, 2023

I've updated the pull request by a split out function (although the tests that were deleted - and no longer are did verify that passing a sealed type serializer should fail for subclass).
@Whathecode If you have the serializer dynamically then you could use that to register all children to the module. Of course this only works if the base sealed type is serializable and you are not pulling all kinds of class loader shenanigans (that are not transparent to the sealed serializer).

@pdvrieze pdvrieze force-pushed the sealed-modules branch 4 times, most recently from d584677 to dfae643 Compare July 4, 2023 09:28
@pdvrieze
Copy link
Contributor Author

@Whathecode The thing this pull request does is support the case where there is a polymorphic base type P, a sealed base B : P and leaf types L1 : B, L2: B, L9: B, etc.. The pull request allows one to, rather than register all leafs (L1…L9) independently as polymorphic child of P, "register" the sealed parent as child of P and all the actually serializable leaves are then registered automatically as serializers for the base type P (the sealed parent is by definition abstract so not can not/should not be registered itself).

@pdvrieze
Copy link
Contributor Author

I've rebased this upon the current dev, and am happy to make needed changes. For clarity this version does not reuse the function name, but has a separate function instead (the original one would not work validly on sealed classes).

@sandwwraith sandwwraith self-assigned this Aug 15, 2024
Copy link
Member

@sandwwraith sandwwraith left a comment

Choose a reason for hiding this comment

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

While this does not completely solve #1865, it is a viable workaround, so I'm in favor of adding such function after addressing all the comments



/**
* Registers the child serializers for the sealed [subclass] [serializer] in the resulting module under the [base class][Base].
Copy link
Member

Choose a reason for hiding this comment

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

Documentation here does not mention that Base has to be sealed.

Also, it would be more helpful if documentation contained the example in which scenario this function has to be used. I think #2199 has a good example with

interface Base

@Serializable
sealed interface Sub 

serializersModule {
  polymorphic(Base::class) {
     subclassesOf<Sub>()
  }
}  

/**
* Registers the child serializers for the sealed [subclass] [serializer] in the resulting module under the [base class][Base].
*/
public inline fun <reified T : Base> subclassesOf(): Unit =
Copy link
Member

Choose a reason for hiding this comment

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

All new functions should be marked with @ExperimentalSerializationApi

* Registers the subclasses of the given class as subclasses of the outer class. This currently requires `baseClass`
* to be sealed.
*/
public fun <T: Base> subclassesOf(serializer: KSerializer<T>) {
Copy link
Member

Choose a reason for hiding this comment

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

After thinking about it for a while, I came to the idea that subclassesOf is a misleading name. It implies that we can extract all subclasses for an arbitrary class, which we can't do — and as we all know, not every person will be reading documentation to find out that we are able to handle only sealed classes. So more appropriate name would be sealedSubclassesOf or subclassesOfSealed or something in that direction.

@rocketraman
Copy link

Any updates on this PR?

@rocketraman
Copy link

rocketraman commented Nov 13, 2025

Bump on this PR... reading through the comments above the changes necessary to get this to the finish line seem pretty trivial -- some docs, an experimental annotation, and a function rename.

@pdvrieze
Copy link
Contributor Author

My apologies for not progressing this earlier, I completely missed this.

I have created an update for this (and rebased it). This update renames the function to subclassesOfSealed (per suggestion, it makes more sense than the other suggestion).

I did add a check that (grand)children are not themselves open polymorphic at module creation time (rather than leaving it to the validation in the format creation - as done in Json). This was chosen over allowing it, but ignoring these children (requiring those to be still registered the normal way). This latter option looks confusing.

Copy link
Member

@sandwwraith sandwwraith left a comment

Choose a reason for hiding this comment

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

Thanks a lot for bringing this back to life! Looking forward to merging this, it should help quickly solve pain points in Compose Navigation


/**
* Registers the child serializers for the sealed [subclass] [serializer] in the resulting module under
* the [base class][Base]. Please note that type `T` must be sealed, if not a runtime error will
Copy link
Member

Choose a reason for hiding this comment

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

IMO "type T has to be sealed and have standard serializer" is slightly better version, since user may declare a custom serializer for sealed type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adapted (ignoring the ability of a custom serializer to mimick sealed serializer sufficiently)

*
*
* Example:
* ```kotlin
Copy link
Member

Choose a reason for hiding this comment

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

IIRC lang tags do not work with Dokka, but that has to be re-checked

}
for ((subsubclass, subserializer) in serializer.class2Serializer.entries) {
// This error would be caught by the Json format in its validation, but this is format specific
require (subserializer.descriptor.kind != PolymorphicKind.OPEN) {
Copy link
Member

Choose a reason for hiding this comment

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

Did you not check PolymorphicKind.SEALED intentionally? I have intuition that this indeed should work:

interface I

@Serializable sealed interface A: I
@Serializable sealed class B: A
@Serializable class Impl: B()

serializersModule {
    polymorphic(I::class) {
        subclassesOfSealed<A>() // should serialize Impl
    }
}

IIRC plugin already flattens hierarchy, so A.class2Serializer will include Impl.serializer().

Please add the corresponding test for that and mention in the documentation that subclasses of sealed subclasses are also collected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code intends to guard against the issue where you have:

interface I
@Serializable sealed interface A : I
@Serializable abstract class B: A // or open
@Serializable class Impl(): B()

In such case Impl would have to be registered independently against base class I. I don't think it would be valid to allow this to be resolved by resolving with B as base class instead. I'll make sure to have the tests include this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that already fails with the serializer for Baz can't be registered as subclass because it is open. While I could "skip" Baz as open or let the failure trigger later, it may be better to throw it here as.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Finally, I did test the code without this check. It actually fails on registration (as an open polymorphic class is registered as subclass). Instead it is possible to "skip" these descendants, and have the leaf types registered, but this is rather confusing in behaviour so I think it is better to disallow this.

// Note that the parameter type is `KSerializer` as `SealedClassSerializer` is an internal type
// not available to users
require(serializer is SealedClassSerializer) {
"subClassesOf only supports automatic adding of subclasses of sealed types."
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"subClassesOf only supports automatic adding of subclasses of sealed types."
subclassesOfSealed only supports automatic adding of subclasses of sealed types with standard serializers.

* }
* ```
*
* It is an error if th
Copy link
Member

Choose a reason for hiding this comment

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

Incomplete comment

*/
@ExperimentalSerializationApi
public inline fun <Base : Any, reified T : Base> PolymorphicModuleBuilder<Base>.subclassesOfSealed(clazz: KClass<T>): Unit =
subclassesOfSealed(clazz.serializer())
Copy link
Member

Choose a reason for hiding this comment

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

should be just serializer() since you already have reified T. I feel like this overload may be not necessary, though. But we may include it for completeness

@Serializable
data class Bar(val bar: Int) : FooOpen()
@Serializable
abstract class Baz: FooOpen()
Copy link
Member

Choose a reason for hiding this comment

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

As I said, please add identical test where this class is sealed and has some inheritors

@Serializable
data class FooHolder(
val someMetadata: Int,
val payload: List<@Polymorphic FooBase>
Copy link
Member

Choose a reason for hiding this comment

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

@Polymorphic is not necessary as abstract classes are polymorphic by default

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

FooHolder(42, listOf(Foo.Bar(1), Foo.Baz("2"))),
FooHolder.serializer(),
json,
printResult = true
Copy link
Member

Choose a reason for hiding this comment

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

This can be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed the printResult bit.

…aled children of a class for polymorphic serialization of a shared (non-sealed) type.
…avoid casting warnings). Use a map to allow faster lookup of serializers to class (to allow for overlapping hierarchies).
- Rename subClassesOf to subClassesOfSealed - also update the function documentation.
- Add a check for open polymorphic children (rather than leaving this to failure on format initialisation/validation).
- Add a test that the open children will throw an exception
- Fix/enhance the documentation extension
- Update the apiDump
- Make it clearer that the hierarchy from the sealed class must only be concrete/sealed. Any open
  descendants fail.
- Revert back to the list of subclasses to be a list, rather than a map.
- Fix some documentation/exception text errors.
- Make the test for open descendants more robust/comprehensive.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants