Skip to content

feat: nested sealed hierarchies for oneOf/anyOf subtypes#28

Closed
halotukozak wants to merge 2 commits intofeat/12-unified-name-registryfrom
feat/nested-sealed-hierarchies
Closed

feat: nested sealed hierarchies for oneOf/anyOf subtypes#28
halotukozak wants to merge 2 commits intofeat/12-unified-name-registryfrom
feat/nested-sealed-hierarchies

Conversation

@halotukozak
Copy link
Member

Summary

  • Restructure polymorphic generation from sealed interfaces + separate files to sealed classes with nested data class subtypes
  • Shape.Circle, Shape.Square instead of separate Circle.kt, Square.kt
  • Add classNameLookup: Map<String, ClassName> to TypeMapping.toTypeName() for nested ClassName resolution
  • Update SerializersModuleGenerator to use parentClass.nestedClass(variant)
  • Wire classNameLookup through CodeGeneratorClientGenerator for endpoint type resolution
  • anyOf-without-discriminator stays as sealed interface (JsonContentPolymorphicSerializer pattern)

Depends on: #27 (feat/12-unified-name-registry)

Test plan

  • ModelGeneratorPolymorphicTest — nested sealed class assertions, one file per hierarchy, no separate variant files
  • SerializersModule uses qualified Shape.Circle::class references
  • @JsonClassDiscriminator on parent, @SerialName on nested subtypes preserved
  • ClientGenerator resolves variant types via classNameLookup
  • Full test suite passing

🤖 Generated with Claude Code

halotukozak and others added 2 commits March 24, 2026 11:04
…archies

- ModelGenerator produces sealed class with nested data class subtypes for oneOf/anyOf-with-discriminator
- TypeMapping.toTypeName accepts classNameLookup for nested ClassName resolution
- SerializersModuleGenerator uses parentClass.nestedClass(variant) for variant references
- anyOf-without-discriminator path unchanged (sealed interface + JsonContentPolymorphicSerializer)
- One FileSpec per hierarchy, variant schemas skipped from top-level generation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…erator

- ClientGenerator accepts classNameLookup for nested ClassName resolution
- All TypeMapping.toTypeName calls in ClientGenerator pass classNameLookup
- CodeGenerator builds classNameLookup via ModelGenerator.buildClassNameLookup companion
- Full test suite passes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 24, 2026 10:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Kotlin model/code generation strategy for polymorphic schemas so that oneOf and anyOf with discriminator generate a sealed class with nested data-class variants (e.g., Shape.Circle, Shape.Square), and wires nested ClassName resolution through TypeMapping into the client generator and serializers module generation.

Changes:

  • Generate polymorphic oneOf / discriminated anyOf as sealed classes with nested variant data classes, producing one file per hierarchy.
  • Extend TypeMapping.toTypeName with an optional classNameLookup to resolve references to nested types.
  • Update serializers module generation and client generation to reference nested variants (e.g., Shape.Circle::class) via the lookup.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorPolymorphicTest.kt Updates/expands tests to assert nested sealed hierarchies and nested serializer references.
core/src/main/kotlin/com/avsystem/justworks/core/gen/TypeMapping.kt Adds classNameLookup support for resolving TypeRef.Reference to nested ClassNames.
core/src/main/kotlin/com/avsystem/justworks/core/gen/SerializersModuleGenerator.kt Switches variant registration from flat ClassName to parentClass.nestedClass(variant).
core/src/main/kotlin/com/avsystem/justworks/core/gen/ModelGenerator.kt Implements sealed class + nested variants generation; filters out separate variant files; adds lookup builder.
core/src/main/kotlin/com/avsystem/justworks/core/gen/CodeGenerator.kt Builds and passes the classNameLookup into ClientGenerator.
core/src/main/kotlin/com/avsystem/justworks/core/gen/ClientGenerator.kt Uses classNameLookup when resolving endpoint parameter and response types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

for (variantName in variants) {
val variantSchema = schemasById[variantName] ?: continue
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

generateSealedHierarchy silently skips a variant when the referenced variant schema isn't present (schemasById[variantName] ?: continue). That can produce an incomplete sealed hierarchy without any signal to the caller. Consider failing fast (e.g., requireNotNull) or surfacing a structured error/warning so missing/typoed variant refs don't generate broken output.

Suggested change
val variantSchema = schemasById[variantName] ?: continue
val variantSchema = requireNotNull(schemasById[variantName]) {
"Missing schema for sealed hierarchy variant '$variantName' in '${schema.name}'"
}

Copilot uses AI. Check for mistakes.
Comment on lines +283 to +289
val constructorBuilder = FunSpec.constructorBuilder()
val propertySpecs = sortedProps.map { prop ->
val type = TypeMapping.toTypeName(prop.type, modelPackage).copy(nullable = prop.nullable)
val kotlinName = prop.name.toCamelCase()

val paramBuilder = ParameterSpec.builder(kotlinName, type)

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

Nested variant properties currently resolve referenced schemas via TypeMapping.toTypeName(prop.type, modelPackage) without passing the new classNameLookup. If a nested variant (or its inline schemas) references another variant schema by name, this will emit a flat ClassName(modelPackage, "Variant") that no longer exists as a top-level type after nesting. Consider computing the lookup once from HierarchyInfo and threading it into these toTypeName calls.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +52
val variantNames = hierarchyInfo.sealedHierarchies
.filterKeys { it !in hierarchyInfo.anyOfWithoutDiscriminator }
.values
.flatten()
.toSet()

val schemaFiles = spec.schemas
.filter { it.name !in variantNames }
.flatMap { generateSchemaFiles(it) }

val inlineSchemaFiles = collectAllInlineSchemas(spec).map {
if (it.isNested) generateNestedInlineClass(it) else generateDataClass(it)
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

generate() now filters out variant schemas from schemaFiles (so Circle.kt/Square.kt etc are no longer generated for discriminated hierarchies). After this change, any remaining TypeRef.Reference("Circle") usage outside the parent hierarchy (e.g., in other schema properties or inline schemas) must resolve to the nested type (Shape.Circle) or the generated model code will reference a missing top-level class. Consider building a classNameLookup from hierarchyInfo and using it consistently inside ModelGenerator (not only in ClientGenerator).

Copilot uses AI. Check for mistakes.
@halotukozak
Copy link
Member Author

Closing — will re-create on top of updated feat/12-unified-name-registry branch to resolve conflicts.

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.

2 participants