Skip to content

Exclude sealed marker types from TypeScript output#1128

Open
almarzn wants to merge 1 commit intovojtechhabarta:mainfrom
almarzn:fix/sealed-interface-marker
Open

Exclude sealed marker types from TypeScript output#1128
almarzn wants to merge 1 commit intovojtechhabarta:mainfrom
almarzn:fix/sealed-interface-marker

Conversation

@almarzn
Copy link
Copy Markdown

@almarzn almarzn commented Apr 19, 2026

Summary

Adds support for excluding sealed interface/class markers from generated TypeScript output.

Problem

When using sealed interfaces to organize a type hierarchy, intermediate sealed types that don't have @JsonSubTypes were being generated as TypeScript interfaces and included in unions, even though they serve only as grouping markers.

Example:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "unit")
public sealed interface Quantity {
    sealed interface DecimalAmount extends Quantity {  // marker, no @JsonSubTypes
        record Gram(BigDecimal amount) implements DecimalAmount {}
        record Kilogram(BigDecimal amount) implements DecimalAmount {}
    }
    record Unspecified(String notes) implements Quantity {}
}

Before this change:

interface Gram extends DecimalAmount { ... }
interface DecimalAmount extends Quantity { ... }  // unwanted
interface Quantity { ... }
type QuantityUnion = Gram | Kilogram | ... | DecimalAmount;  // marker in union

After this change:

interface Gram extends Quantity { ... }  // extends root directly
interface Quantity { ... }
type QuantityUnion = Gram | Kilogram | ... | Unspecified;  // no marker

Solution

Added excludeSealedMarkerTypes() transformation in ModelCompiler that:

  1. Identifies sealed types without @JsonSubTypes or @JsonTypeInfo as markers
  2. Removes them from the model
  3. Replaces parent/extends references to markers with the marker's parent
  4. Expands markers in tagged unions to their permitted subclasses

Changes

  • ModelCompiler.java: Added excludeSealedMarkerTypes(), isSealedMarker(), getSealedPermittedSubclasses(), findNonSealedParent()
  • TsBeanModel.java: Added withParent() method
  • SealedInterfaceTest.java: Added test for sealed marker exclusion

Testing

  • New test SealedInterfaceTest.testSealedInterfaceMarkerExcluded() passes
  • All 378 existing tests pass

Sealed interfaces/classes without @JsonSubTypes are considered 'markers'
that just group related types. These should not appear in the generated
TypeScript output.

For example, with this hierarchy:
  @JsonTypeInfo sealed interface Quantity
    sealed interface DecimalAmount extends Quantity
      record Gram implements DecimalAmount
      record Kilogram implements DecimalAmount
    record Unspecified implements Quantity

Before:
  interface Gram extends DecimalAmount
  interface DecimalAmount extends Quantity
  type QuantityUnion = Gram | Kilogram | ... | DecimalAmount

After:
  interface Gram extends Quantity
  type QuantityUnion = Gram | Kilogram | ... | Unspecified

Changes:
- Add excludeSealedMarkerTypes() in ModelCompiler
- Add isSealedMarker() to detect sealed types without Jackson annotations
- Add getSealedPermittedSubclasses() to expand nested sealed markers
- Add findNonSealedParent() to skip markers in inheritance chain
- Add withParent() method to TsBeanModel
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant